diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..3390c9e54
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,13 @@
+version: 2
+updates:
+ - package-ecosystem: "npm"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+ cooldown:
+ default-days: 1
+ include:
+ - "*"
+ exclude:
+ - "@conduction/*"
diff --git a/.github/workflows/app-downloads.yml b/.github/workflows/app-downloads.yml
deleted file mode 100644
index 63672312e..000000000
--- a/.github/workflows/app-downloads.yml
+++ /dev/null
@@ -1,40 +0,0 @@
-name: App Downloads
-
-# Refreshes data/app-downloads.json with GitHub release-asset download counts
-# and Nextcloud app store metadata for every ConductionNL app.
-# Cron is UTC; 07:00 UTC = 09:00 Amsterdam (CEST) / 08:00 (CET).
-
-on:
- schedule:
- - cron: '0 7 * * 1-5'
- workflow_dispatch:
-
-permissions:
- contents: write
-
-jobs:
- collect:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - uses: actions/setup-python@v5
- with:
- python-version: '3.11'
-
- - name: Collect download stats
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: python .github/scripts/app_downloads.py
-
- - name: Commit if changed
- run: |
- git config user.name "github-actions[bot]"
- git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- git add data/app-downloads.json
- if git diff --cached --quiet; then
- echo "No changes."
- else
- git commit -m "chore: update app download stats"
- git push
- fi
diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml
index 126555915..81cf271c6 100644
--- a/.github/workflows/documentation.yml
+++ b/.github/workflows/documentation.yml
@@ -5,6 +5,15 @@ on:
branches: [development, documentation]
pull_request:
branches: [development, documentation]
+ # Nightly rebuild so app-downloads.json (regenerated by the npm
+ # prebuild step) reflects fresh GitHub release-asset counts. 07:00
+ # UTC = 09:00 Amsterdam (CEST) / 08:00 (CET). Replaces the old
+ # app-downloads.yml that committed the JSON to development; the
+ # PR-required ruleset blocked the bot push. Build-time fetch
+ # sidesteps the bot-push problem entirely.
+ schedule:
+ - cron: '0 7 * * *'
+ workflow_dispatch:
jobs:
deploy:
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 000000000..3942d3489
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,5 @@
+# Supply-chain hardening: reject any npm package published less than
+# 24h ago. Compromised first-party-Conduction packages are excluded via
+# Dependabot cooldown (.github/dependabot.yml); for fresh @conduction/*
+# releases, override per-install with `npm install --min-release-age=0`.
+min-release-age=1
diff --git a/README.md b/README.md
index c5aa7ca98..fc0bac8cf 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,9 @@
The Docusaurus 3 site for [conduction.nl](https://conduction.nl). Built on `@conduction/docusaurus-preset` for brand defaults; site-specific config lives in [docusaurus.config.js](docusaurus.config.js).
-It also serves the `connext.conduction.nl` and `commonground.conduction.nl` vanity entry points (Cloudflare 301-redirects to `/connext` and `/commonground` paths on this site, with locale awareness via `Accept-Language`).
+It also serves the `connext.conduction.nl` and `commonground.conduction.nl` vanity entry points (a Cloudflare worker — configured in the dashboard, not in this repo — 301-redirects to the `/connext` and `/commonground` paths on this site, with locale awareness via `Accept-Language`).
+
+> **Vanity-domain redirect target:** the worker must redirect to `https://www.conduction.nl/connext/` — with the `www.` and the **trailing slash**. The site is built with `url: https://www.conduction.nl` (matching `static/CNAME`) and `trailingSlash: true`. Targeting the bare apex (`conduction.nl`) or the slash-less path (`/connext`) stacks extra GitHub Pages 301s on every request and, if the worker route also matches `www`, can produce a redirect loop. If you see a redirect loop on `connext.conduction.nl`, also check Cloudflare's SSL/TLS mode is **Full** (not "Flexible") — Flexible + GitHub Pages "Enforce HTTPS" is the classic infinite HTTP↔HTTPS loop.
## Local development
diff --git a/academy/2026-05-07-nextcloud-lokaal-draaien/index.mdx b/academy/2026-05-07-nextcloud-lokaal-draaien/index.mdx
index a910022f4..07395d416 100644
--- a/academy/2026-05-07-nextcloud-lokaal-draaien/index.mdx
+++ b/academy/2026-05-07-nextcloud-lokaal-draaien/index.mdx
@@ -17,7 +17,7 @@ Voor elke andere tutorial in deze academy heb je een werkende Nextcloud nodig. D
Een werkende Nextcloud op http://localhost:8080 opzetten met Docker.
- Postgres voor data, Redis voor cache en Nextcloud voor het werk in één compose-bestand combineren.
+ Postgres voor data, Redis voor cache, Nextcloud voor het werk en notify_push voor realtime updates in één compose-bestand combineren.Een admin-account aanmaken waar je naar believen Conduction-apps op installeert.Met één commando de hele stack weer opruimen.
@@ -90,12 +90,25 @@ services:
REDIS_HOST: redis
NEXTCLOUD_TRUSTED_DOMAINS: "localhost"
+ notify_push:
+ image: icewind1991/notify_push:latest
+ restart: unless-stopped
+ depends_on: [nextcloud, db, redis]
+ ports:
+ - "7867:7867"
+ volumes:
+ - nextcloud:/nextcloud:ro
+ environment:
+ NEXTCLOUD_URL: http://nextcloud
+ DATABASE_URL: postgres://nextcloud:nextcloud@db/nextcloud
+ REDIS_URL: redis://redis:6379
+
volumes:
db:
nextcloud:
```
-Drie services, allemaal officiële images, geen custom build. De `./apps`-bind-mount is optioneel: handig als je een Conduction-app als zip naast de installatie wilt zetten in plaats van via de app store.
+Vier services, allemaal officiële images, geen custom build. De `./apps`-bind-mount is optioneel: handig als je een Conduction-app als zip naast de installatie wilt zetten in plaats van via de app store. `notify_push` is de WebSocket push-server die Conduction-apps gebruiken voor realtime updates — meer daarover in stap 5.
## Stap 2: Start de stack
@@ -133,6 +146,32 @@ Klik je avatar rechtsboven en kies **Apps**. De Conduction-apps staan onder **In
Na elke install draait de schema-bootstrap automatisch. Voorbeelddata is binnen seconden zichtbaar.
+Installeer ook de `notify_push`-app (onder **Integration**). Die is de Nextcloud-zijde van de WebSocket push-server die je in stap 1 al hebt gestart — je hoeft hem alleen te enabelen, geen verdere config nodig.
+
+## Stap 5: Koppel `notify_push` aan de push-server
+
+`notify_push` heeft één eenmalige setup-stap zodat de Nextcloud-app weet waar de Rust-binary draait. Voer in de werkmap uit:
+
+```bash
+docker compose exec --user www-data nextcloud php occ config:system:set redis host --value redis
+docker compose exec --user www-data nextcloud php occ config:system:set redis port --value 6379 --type integer
+docker compose exec --user www-data nextcloud php occ config:system:set memcache.distributed --value '\OC\Memcache\Redis'
+docker compose exec --user www-data nextcloud php occ config:system:set trusted_proxies 0 --value 172.16.0.0/12
+docker compose exec --user www-data nextcloud php occ notify_push:setup http://notify_push:7867
+```
+
+De eerste drie regels vertellen Nextcloud dat Redis de gedeelde cache is — `notify_push` luistert daarop voor change-events. De vierde regel markeert het Docker-netwerk als trusted zodat de push-server bij Nextcloud terugmag bellen voor authenticatie. De laatste regel registreert de URL bij de Nextcloud-app.
+
+Verifieer dat alles werkt:
+
+```bash
+docker compose exec --user www-data nextcloud php occ notify_push:self-test
+```
+
+Je krijgt een lijstje vinkjes — `redis is configured`, `push server is receiving redis messages`, `push server can connect to the Nextcloud server`, et cetera. De waarschuwing over `unencrypted http` is voor lokaal werk normaal; in productie zit `notify_push` achter dezelfde reverse proxy als Nextcloud, dus dan is dat geen issue.
+
+Vanaf nu krijgen apps die op `notify_push` luisteren — zoals OpenRegister's live-update-integratie via `@conduction/nextcloud-vue` — direct doorduwingen op object-wijzigingen, zonder dat de browser hoeft te pollen.
+
## Wat je krijgt
| Eigenschap | Waarde |
@@ -170,6 +209,10 @@ Na `down -v` is je laptop weer schoon. Twee minuten later kun je opnieuw beginne
**App-store install werkt niet:** controleer of Nextcloud naar buiten kan. In een corporate netwerk soms blokkade op `apps.nextcloud.com`. Een proxy via `HTTP_PROXY`-env-variabelen lost dit op.
+**`notify_push` self-test faalt op `is not trusted as a reverse proxy`:** het Docker-netwerk-bereik in jouw setup wijkt af van `172.16.0.0/12`. Bekijk het bereik met `docker network inspect $(docker network ls --filter name=conduction-demo -q) --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'` en pas de `trusted_proxies`-regel uit stap 5 aan met die subnet.
+
+**`notify_push` blijft restarten na `docker compose up -d`:** kijk in de logs (`docker compose logs notify_push`) of je `× No redis server is configured` ziet. Dat betekent dat Nextcloud's `memcache.distributed`-config nog niet op Redis staat — voer de eerste drie `occ config:system:set`-commando's uit stap 5 uit, en herstart dan met `docker compose up -d notify_push`.
+
## Volgende stap
Een werkende Nextcloud is de aanleiding voor de andere tutorials in deze serie:
diff --git a/academy/2026-05-10-deskdesk-tutorial-1-scaffold/index.mdx b/academy/2026-05-10-deskdesk-tutorial-1-scaffold/index.mdx
new file mode 100644
index 000000000..50889b464
--- /dev/null
+++ b/academy/2026-05-10-deskdesk-tutorial-1-scaffold/index.mdx
@@ -0,0 +1,216 @@
+---
+slug: deskdesk-tutorial-1-scaffold
+title: "Build a Nextcloud app on the Conduction stack — Part 1: Scaffold"
+contentType: tutorial
+authors: [conduction]
+date: 2026-05-10
+summary: Clone the Conduction app template, rename it to DeskDesk, build, enable, and see the canonical app chassis. The first of four parts that build a working desk-booking app on the full Conduction stack.
+tags: [App development, Nextcloud, OpenRegister, nextcloud-vue, Tutorial series]
+apps: [openregister]
+durationMinutes: 20
+series: deskdesk-tutorial
+partNumber: 1
+---
+
+import {
+ Outcomes,
+ Outcome,
+ Prerequisites,
+ PrerequisiteItem,
+ AtomZones,
+ NextSteps,
+ NextStep,
+ HexCard,
+} from '@conduction/docusaurus-preset/components';
+
+This is **Part 1 of a four-part tutorial** that builds **DeskDesk**, a flexible desk-booking app for an open-office environment, on the full Conduction Nextcloud stack. The end product: pick a desk on a floor, book a slot, see your booking in your Nextcloud Calendar, and surface zone-specific knowledge articles from xWiki right next to the booking. Every piece reuses what `@conduction/nextcloud-vue` and OpenRegister already give you for free.
+
+In Part 1 you scaffold the app, rename it, build it, and see the canonical app chassis on screen. No data, no integrations yet. We get the bones right first.
+
+{/* truncate */}
+
+
+ A working DeskDesk app installed in your local Nextcloud, navigable from the app shelf.
+ A clear mental model of the canonical Conduction app chassis: topbar, left navigation, main column, page header, sidebar.
+ Confidence with the nextcloud-app-template rename ritual: id, namespace, the few files you have to touch, the rest you can leave for later.
+ A clean foundation for Part 2, where the chassis starts holding real data.
+
+
+## What we're building, across all four parts
+
+| Part | Title | Adds |
+|---|---|---|
+| 1 | Scaffold (you're here) | Empty app shell, chassis visible, navigable |
+| 2 | Schemas + manifest | Desk and booking schemas, full CRUD, dashboard |
+| 3 | Schema-driven integrations | Bookings appear in NC Calendar via the OpenRegister calendar provider |
+| 4 | External knowledge + ship | xWiki source via OpenConnector, sidebar tab, package, publish |
+
+Same shape, two repos:
+
+- **App source:** [`ConductionNL/deskdesk`](https://github.com/ConductionNL/deskdesk) — the actual Nextcloud app you're forking from the template.
+- **Tutorial source:** [`ConductionNL/conduction-website`](https://github.com/ConductionNL/conduction-website) — the post you're reading right now.
+
+
+ A running Nextcloud (28+) with admin access. If you don't have one, follow Nextcloud lokaal draaien met Docker first.
+ OpenRegister installed and enabled in that Nextcloud (Apps → search "OpenRegister" → Download).
+ Node.js 20+ and PHP 8.1+ on your host, plus Composer and the GitHub CLI (gh) — the template ships with PHPCS, Psalm, PHPStan, and ESLint configs ready to go.
+ A GitHub account so you can fork the template into your own org (or ConductionNL if you have access).
+
+
+## Step 1: Use the template
+
+[`ConductionNL/nextcloud-app-template`](https://github.com/ConductionNL/nextcloud-app-template) is a GitHub *repository template*. The "Use this template" button gives you a fresh repo with the full starter kit: Vue 2 + Pinia frontend, PHP backend, OpenRegister wiring, quality pipeline, OpenSpec scaffolding, GitHub Actions.
+
+```bash title="Pick a name. We're calling ours deskdesk."
+gh repo create ConductionNL/deskdesk \
+ --template ConductionNL/nextcloud-app-template \
+ --public \
+ --description "Flexible desk booking for open-office environments"
+```
+
+Then clone it next to your other Nextcloud apps. Most workspaces keep them in a single `apps-extra/` directory the dev container mounts.
+
+```bash
+cd /path/to/your/nextcloud/workspace/apps-extra
+git clone https://github.com/ConductionNL/deskdesk.git
+cd deskdesk
+```
+
+You now have a directory with the full template content under the new name. Nothing inside has been renamed yet — the directory is `deskdesk/`, but every file still says `app-template`, `AppTemplate`, `OCA\AppTemplate`. Step 2 fixes that.
+
+## Step 2: Rename the app
+
+Nextcloud requires three identifiers to line up:
+
+- The directory name (`deskdesk/`) ✅ already
+- The `` in `appinfo/info.xml`
+- The PHP namespace (`OCA\AppTemplate` → `OCA\DeskDesk`)
+
+Plus a handful of supporting files reference the old id. The full list is small enough to do by hand, and doing it by hand is the right move. Project memory: **never use `sed` or scripted edits on code files** — use a real editor with project-aware refactoring, or just read each file once before you change it.
+
+### 2a. The boot-critical files
+
+These are the ones that prevent Nextcloud from booting the app at all. Edit each with `Find & replace all` in your editor — `AppTemplate` → `DeskDesk` and `app-template` → `deskdesk`:
+
+| File | Replace |
+|---|---|
+| `appinfo/info.xml` | ``, ``, ``, ``, `` paths |
+| `composer.json` | `name`, `description`, the `psr-4` autoload prefix |
+| `package.json` | `name` |
+| `webpack.config.js` | the `appId` constant |
+| `templates/index.php` and `templates/settings/admin.php` | `OCA\AppTemplate` → `OCA\DeskDesk`, the `data-*` element id |
+| `lib/AppInfo/Application.php` | namespace, `APP_ID` constant, docblock |
+| Every other PHP file in `lib/` | `namespace OCA\AppTemplate\…` → `namespace OCA\DeskDesk\…`, every `use OCA\AppTemplate\…` import, every docblock |
+| `appinfo/routes.php` | docblock only |
+| Every Vue file in `src/` | the `t('app-template', '…')` translation namespace becomes `t('deskdesk', '…')` |
+| `src/router/index.js` | `generateUrl('/apps/app-template')` → `generateUrl('/apps/deskdesk')` |
+| `src/store/store.js`, `src/store/modules/settings.js` | `'/apps/app-template/api/settings'` → `'/apps/deskdesk/api/settings'`, fallback register slug |
+| `src/settings.js` | `loadTranslations('app-template', …)` and the `#app-template-settings` mount selector |
+| `lib/Settings/app_template_register.json` | rename to `lib/Settings/deskdesk_register.json` (and the `app` field inside it) |
+
+### 2b. The "you can do later" files
+
+Tests (`tests/Unit/AppTemplateTest.php`, the integration Postman collection), `phpcs.xml` / `phpmd.xml` / `REUSE.toml` headers, and the `.github/` workflow inputs. They reference the template id in metadata only; the app boots fine without them touched. Fix them when you set up CI.
+
+### 2c. One Nextcloud-version compatibility nip
+
+The template's `lib/AppInfo/Application.php` registers a repair step at runtime:
+
+```php title="Original — fails on some Nextcloud builds"
+$context->registerRepairStep(InitializeSettings::class);
+```
+
+`registerRepairStep()` is missing from `IRegistrationContext` on a few Nextcloud builds (you'll see `Call to undefined method` in `nextcloud.log`). Move it to `appinfo/info.xml` instead — same effect, more portable:
+
+```xml title="appinfo/info.xml — append after the block"
+
+
+ OCA\DeskDesk\Repair\InitializeSettings
+
+
+ OCA\DeskDesk\Repair\InitializeSettings
+
+
+```
+
+Then drop the `registerRepairStep()` line and the `use OCA\DeskDesk\Repair\InitializeSettings;` import from `lib/AppInfo/Application.php`.
+
+## Step 3: Build and enable
+
+The template ships its build output uncommitted. So the first thing to do in a fresh clone is install dependencies and build the JS bundles, otherwise the app UI is just a blank `
`.
+
+```bash
+composer install --no-dev
+composer dump-autoload
+npm install --legacy-peer-deps
+npm run build
+```
+
+Then make sure your Nextcloud container can see the directory. In a typical Docker setup the `apps-extra/` host directory is mounted at `/var/www/html/custom_apps/` in the container; if your compose file uses one bind mount per app, add a line for `deskdesk` and restart. Otherwise:
+
+```bash title="Quick path: copy + chown for a one-shot install"
+docker cp ./deskdesk nextcloud:/var/www/html/custom_apps/
+docker exec -u root nextcloud chown -R www-data:www-data /var/www/html/custom_apps/deskdesk
+```
+
+Now enable:
+
+```bash
+docker exec nextcloud php occ app:enable deskdesk
+```
+
+You should see:
+
+```
+deskdesk 0.1.0 enabled
+```
+
+Open `http://localhost:8080/apps/deskdesk/` and log in. You see a placeholder dashboard with sample KPI cards, two empty panels, three nav items (Dashboard / Items / Documentation) and a Settings entry pinned to the bottom of the rail. That's the chassis.
+
+## The chassis: the whole point of Part 1
+
+Every Conduction app — DeskDesk, OpenRegister, OpenCatalogi, Procest, MyDash, the dozen others — looks the same way on first sight. Same five structural pieces, same place, same behaviour. That recognisability is what `@conduction/nextcloud-vue` enforces: a user who learnt one app navigates the next one without docs.
+
+The chassis is **one shape, five atoms**. Each card below focuses on one atom: the focused zone is at full opacity with a KNVB-orange outline, the rest fades to 25% so you see *where* the atom sits.
+
+
+
+In Part 2, you'll learn how `manifest.json` plus a JSON schema fills these atoms with real data. The placeholder `Items` nav entry, the empty dashboard, and the "article" schema you saw on screen will all become **desks**, **bookings**, and a real occupancy dashboard — without you laying out a single atom by hand.
+
+## Troubleshooting
+
+
+ The js/ build output is not committed. Run npm run build before you enable the app, or after every frontend change. The blank-rectangle symptom always means "the bundle is missing".
+
+
+
+ Nextcloud requires the directory name to exactly match the <id> in appinfo/info.xml. If you cloned the repo under a different name, either rename the directory or add a relative symlink: ln -s your-clone-name ../deskdesk.
+
+
+
+ Step 2c covers this. The fix is to move the repair-step registration from lib/AppInfo/Application.php into appinfo/info.xml's <repair-steps> block.
+
+
+
+ Run composer dump-autoload from the app directory. PHP's classmap caches the old OCA\AppTemplate map until you regenerate it.
+
+
+## What's next
+
+
+
+
+
+
diff --git a/academy/2026-05-13-deskdesk-tutorial-2-schemas-manifest/index.mdx b/academy/2026-05-13-deskdesk-tutorial-2-schemas-manifest/index.mdx
new file mode 100644
index 000000000..fc54a3439
--- /dev/null
+++ b/academy/2026-05-13-deskdesk-tutorial-2-schemas-manifest/index.mdx
@@ -0,0 +1,303 @@
+---
+slug: deskdesk-tutorial-2-schemas-manifest
+title: "Build a Nextcloud app on the Conduction stack — Part 2: Schemas + manifest"
+contentType: tutorial
+authors: [conduction]
+date: 2026-05-13
+summary: Define the desk, booking, and floor schemas in OpenRegister, declare them in a manifest.json, and watch CnAppRoot replace ~200 lines of hand-rolled Vue with three lines of config.
+tags: [App development, OpenRegister, Schemas, Manifest, nextcloud-vue]
+apps: [openregister]
+durationMinutes: 25
+series: deskdesk-tutorial
+partNumber: 2
+---
+
+import {
+ Outcomes,
+ Outcome,
+ Prerequisites,
+ PrerequisiteItem,
+ NextSteps,
+ NextStep,
+ HexCard,
+} from '@conduction/docusaurus-preset/components';
+
+This is **Part 2 of the four-part DeskDesk tutorial**. Part 1 left you with an empty app shell — a chassis, no data. Part 2 fills the chassis: three schemas, a manifest, and the same five Cn* pages drive every list, every detail view, every dashboard with the schema as the single source of truth.
+
+The shape we keep saying "this saves you code" finally has numbers behind it: ~200 lines of hand-rolled Vue collapse to three.
+
+{/* truncate */}
+
+
+ Three OpenRegister schemas — floor, desk, booking — installed and seeded with realistic demo data on first boot.
+ A src/manifest.json that declares the navigation, the routes, and the configuration for every page. CnAppRoot reads it; CnPageRenderer dispatches.
+ An App.vue of ~10 lines and a router/index.js that generates routes from the manifest. navigation/MainMenu.vue is gone. So is views/Dashboard.vue, views/items/*, and the user-settings shells. The manifest renderer replaces them.
+ Index pages, detail pages, and CRUD dialogs that the schema generates — sortable, filterable, searchable — without you writing a column definition.
+
+
+
+ Part 1 done: DeskDesk installed, the chassis renders at http://localhost:8080/apps/deskdesk/.
+ OpenRegister installed, enabled, and at version 0.2.10 or newer (the importFromFilePath ConfigurationService method is required).
+ The local clone of ConductionNL/deskdesk on your filesystem and a working npm run build.
+
+
+## Step 1: Define the three schemas
+
+Open `lib/Settings/deskdesk_register.json` and replace the placeholder `article` schema with the real three. Each schema is a JSON Schema with a couple of OpenRegister extensions: `slug` for the URL identifier, `icon` for the MDI glyph the UI uses, and `x-openregister-relations` for typed cross-schema links.
+
+```json title="lib/Settings/deskdesk_register.json (excerpt)"
+"floor": {
+ "slug": "floor",
+ "title": "Floor",
+ "description": "A physical floor in a building.",
+ "type": "object",
+ "required": ["label"],
+ "properties": {
+ "label": { "type": "string", "example": "Floor 3" },
+ "building": { "type": "string", "example": "Amsterdam HQ" },
+ "planImage": { "type": "string", "format": "uri" }
+ }
+}
+```
+
+The `desk` schema adds the typed relation to floor:
+
+```json title="lib/Settings/deskdesk_register.json (excerpt)"
+"desk": {
+ "slug": "desk",
+ "title": "Desk",
+ "type": "object",
+ "required": ["label", "floor", "zone"],
+ "properties": {
+ "label": { "type": "string", "example": "3-East-12" },
+ "floor": {
+ "type": "string",
+ "x-openregister-relations": {
+ "schema": "floor",
+ "cardinality": "many-to-one"
+ }
+ },
+ "zone": {
+ "type": "string",
+ "enum": ["north", "south", "east", "west", "central"]
+ },
+ "equipment": {
+ "type": "array",
+ "items": { "type": "string", "enum": ["dual-monitor", "standing", "phonebooth-adjacent", "wired-network", "extra-power"] }
+ },
+ "capacity": { "type": "integer", "minimum": 1, "default": 1 },
+ "accessibility": { "type": "boolean", "default": false },
+ "photo": { "type": "string", "format": "uri" },
+ "notes": { "type": "string" }
+ }
+}
+```
+
+Booking is the same shape — a relation, a couple of date-times, an enum status, and an optional RRULE for recurring bookings.
+
+The full file ships in [the deskdesk repo](https://github.com/ConductionNL/deskdesk/blob/main/lib/Settings/deskdesk_register.json), with five seed desks, two floors, and three bookings so the app is browsable on first install.
+
+## Step 2: Declare the register itself
+
+OpenRegister's import handler creates schemas eagerly, but it only creates a **register** when the JSON declares one explicitly. Add a `components.registers` block above your schemas:
+
+```json title="lib/Settings/deskdesk_register.json"
+"components": {
+ "registers": {
+ "deskdesk": {
+ "slug": "deskdesk",
+ "title": "DeskDesk",
+ "description": "Floors, desks, and bookings for the DeskDesk app.",
+ "version": "0.2.0",
+ "schemas": ["floor", "desk", "booking"]
+ }
+ },
+ "schemas": { /* ... */ },
+ "objects": { /* ... */ }
+}
+```
+
+The `schemas` array tells OpenRegister which of the schemas in the same file belong to this register. The `slug` becomes the register's stable identifier.
+
+## Step 3: Trigger the import
+
+Two ways. Either trigger it via the SettingsController (the app exposes `POST /api/settings/load`):
+
+```bash title="From your shell, via curl, with a logged-in cookie jar"
+curl -X POST -b cookies.txt \
+ -H "requesttoken: $REQUESTTOKEN" \
+ http://localhost:8080/index.php/apps/deskdesk/api/settings/load
+```
+
+Or — cleaner during local development — disable + re-enable the app, which fires the `` repair step from `appinfo/info.xml`:
+
+```bash
+docker exec nextcloud php occ app:disable deskdesk
+docker exec nextcloud php occ app:enable deskdesk
+```
+
+Either way, you should see in the OpenRegister registers list:
+
+```
+slug: deskdesk title: DeskDesk application: deskdesk schemas: floor, desk, booking
+```
+
+The matching path the SettingsService takes is **importFromFilePath** with a path resolved relative to `\OC::$SERVERROOT` (which is `/var/www/html` in a default install). The boot-time bug I hit during this tutorial — `Configuration file not found: html/custom_apps/deskdesk/...` — was a relative-path mistake; the fix lands in [the diff](https://github.com/ConductionNL/deskdesk/pull/2/files#diff-lib-Service-SettingsService.php).
+
+## Step 4: The manifest
+
+`src/manifest.json` is the single declarative description of the app shell. Three top-level keys: `dependencies`, `menu`, `pages`.
+
+```json title="src/manifest.json"
+{
+ "$schema": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest.schema.json",
+ "version": "1.0.0",
+ "id": "deskdesk",
+ "title": "DeskDesk",
+ "dependencies": ["openregister"],
+ "menu": [
+ {"id": "desks-index", "label": "deskdesk.menu.desks", "icon": "icon-category-organization", "route": "desks-index", "order": 10},
+ {"id": "bookings-index", "label": "deskdesk.menu.bookings", "icon": "icon-calendar", "route": "bookings-index", "order": 20},
+ {"id": "floors-index", "label": "deskdesk.menu.floors", "icon": "icon-category-monitoring", "route": "floors-index", "order": 30}
+ ],
+ "pages": [
+ {"id": "desks-index", "route": "/", "type": "index", "title": "deskdesk.desks.indexTitle",
+ "config": {"register": "deskdesk", "schema": "desk", "columns": ["label","floor","zone","equipment","capacity","accessibility"], "filters": ["floor","zone","equipment","accessibility"], "defaultSort": {"key": "label", "order": "asc"}}},
+ {"id": "desks-detail", "route": "/desks/:id", "type": "detail", "title": "deskdesk.desks.detailTitle",
+ "config": {"register": "deskdesk", "schema": "desk"}}
+ /* bookings + floors pages follow the same pattern */
+ ]
+}
+```
+
+A few rules to note:
+
+- **`page.id` is the vue-router route name.** CnPageRenderer matches `$route.name === page.id` to pick the right page.
+- **`page.type` is closed.** `index | detail | dashboard | custom`. Use `custom` with a `component` registry entry for one-off pages.
+- **`page.config` is forwarded as props.** `register`, `schema`, `columns`, `filters`, `defaultSort` map to CnIndexPage props one-to-one.
+
+## Step 5: Replace App.vue and the router
+
+Now the satisfying part. Open `src/App.vue` — the 175-line shell from Part 1 — and replace it with this:
+
+```vue title="src/App.vue"
+
+
+
+
+
+```
+
+That's it. The ``, the dependency-check empty-state, the loading spinner, the menu, the sidebars, the user-settings dialog — CnAppRoot ships them all and gates them on the manifest's `dependencies` array.
+
+Then `src/router/index.js` becomes a manifest-to-route mapping:
+
+```js title="src/router/index.js"
+import Vue from 'vue'
+import Router from 'vue-router'
+import { generateUrl } from '@nextcloud/router'
+import { CnPageRenderer } from '@conduction/nextcloud-vue'
+import manifest from '../manifest.json'
+
+Vue.use(Router)
+
+const routes = manifest.pages.map((page) => ({
+ name: page.id,
+ path: page.route,
+ component: CnPageRenderer,
+}))
+
+routes.push({ path: '*', redirect: '/' })
+
+export default new Router({
+ mode: 'history',
+ base: generateUrl('/apps/deskdesk'),
+ routes,
+})
+```
+
+Every page in the manifest gets a route. Every route renders CnPageRenderer. CnPageRenderer reads the manifest, finds the matching page entry by route name, and dispatches by `page.type`:
+
+- `type: "index"` → CnIndexPage with the schema's columns + filters
+- `type: "detail"` → CnDetailPage with the schema's properties
+- `type: "dashboard"` → CnDashboardPage with the layout
+- `type: "custom"` → your registered component
+
+## Step 6: Delete the old views
+
+The whole point of Part 2 is that the framework writes the views for you. Delete:
+
+- `src/navigation/MainMenu.vue` — replaced by CnAppNav (auto-mounted by CnAppRoot)
+- `src/views/Dashboard.vue` — placeholder; will return in a richer form when the dashboard page wants it
+- `src/views/items/ItemList.vue`, `src/views/items/ItemDetail.vue` — replaced by CnIndexPage + CnDetailPage
+- `src/views/settings/UserSettings.vue`, `src/views/settings/Settings.vue` — settings live on the admin page (`AdminRoot.vue`) and the in-app dialog ships with CnAppRoot
+
+The before-and-after on disk:
+
+```
+before: 12 .vue files in src/, ~600 lines of hand-rolled component code
+after: 3 .vue files (App.vue, AdminRoot.vue, that's it), ~30 lines
+```
+
+## Step 7: Build, reload, browse
+
+```bash
+npm run build
+docker cp ./js nextcloud:/var/www/html/custom_apps/deskdesk/
+docker exec nextcloud apache2ctl graceful
+```
+
+Open `/apps/deskdesk/`. The left rail now reads **Desks · Bookings · Floors** (the translation keys will look like `deskdesk.menu.desks` until you add the `l10n/` entries — that's a follow-up). The main column shows CnIndexPage with the schema's columns. Click a row, you're on CnDetailPage for that desk. The whole CRUD flow comes for free.
+
+## Why this matters
+
+You wrote three JSON files. The framework reads them and renders the app. There is no `DesksList.vue`, no `DeskForm.vue`, no `useDesks.js`. When you add a `priority` field to the booking schema, every place a booking appears — table column, detail row, filter sidebar, form dialog — picks up the new field automatically.
+
+This is the schema-driven discipline talked about on [nextcloud-vue.conduction.nl](https://nextcloud-vue.conduction.nl/docs/architecture/schemas-and-registers/). Part 3 puts it to work in a way that surprises people the first time: a single JSON block on the booking schema makes every booking appear in the user's Nextcloud Calendar, no controller, no event, no listener.
+
+## Troubleshooting
+
+
+ The relative path used by ConfigurationService::importFromFilePath is computed against /var/www/html (Nextcloud's SERVERROOT). If your SettingsService strips the wrong prefix you get this error. Use \OC::$SERVERROOT as the anchor when stripping.
+
+
+
+ Add a components.registers.YOURAPP block to the JSON. Without it, the import handler creates the schemas as orphans. The block needs slug, title, version, and schemas[].
+
+
+
+ CnIndexPage's store wants the numeric register id, while /api/settings typically returns the register slug. Wire a slug → id resolution in your settings store, or pass the numeric id from the SettingsService directly. Tracked as a follow-up in the Part 2 PR.
+
+
+
+ The keys (e.g. deskdesk.menu.desks) need entries in l10n/en.json and l10n/nl.json. The library renders the key when no translation is found. Add them whenever you're ready; the app works either way.
+
+
+## What's next
+
+
+
+
+
+
diff --git a/academy/2026-05-15-deskdesk-tutorial-3-calendar/index.mdx b/academy/2026-05-15-deskdesk-tutorial-3-calendar/index.mdx
new file mode 100644
index 000000000..74c7aaee4
--- /dev/null
+++ b/academy/2026-05-15-deskdesk-tutorial-3-calendar/index.mdx
@@ -0,0 +1,184 @@
+---
+slug: deskdesk-tutorial-3-calendar
+title: "Build a Nextcloud app on the Conduction stack — Part 3: Schema-driven integrations"
+contentType: tutorial
+authors: [conduction]
+date: 2026-05-15
+summary: One calendarProvider block on the booking schema. Bookings appear in NC Calendar with no controller, no event listener, no per-app glue.
+tags: [App development, OpenRegister, NC Calendar, CalDAV, Schema integrations]
+apps: [openregister]
+durationMinutes: 15
+series: deskdesk-tutorial
+partNumber: 3
+---
+
+import {
+ Outcomes,
+ Outcome,
+ Prerequisites,
+ PrerequisiteItem,
+ NextSteps,
+ NextStep,
+ HexCard,
+} from '@conduction/docusaurus-preset/components';
+
+This is **Part 3 of the four-part DeskDesk tutorial**. Part 2 wired the desk and booking schemas. Part 3 makes those bookings appear in every user's Nextcloud Calendar — *without writing a controller, an event listener, or any per-app calendar glue*.
+
+The trick: OpenRegister already ships an `ICalendarProvider` (`RegisterCalendarProvider`) that exposes any schema with a `calendarProvider` block as a virtual calendar. You declare the block on the booking schema. NC Calendar shows the events. That's the whole story.
+
+{/* truncate */}
+
+
+ Every booking object in DeskDesk shows up as a read-only event in the Nextcloud Calendar app, with the desk label + user in the title and the cobalt brand colour.
+ A working understanding of OpenRegister's schema-driven integration providers: the same approach unlocks Activity, Mail Smart Picker, Contacts matching, and File actions — all without per-app code.
+ A pattern you can reuse on any future schema. Add a date field + a calendarProvider block. Done.
+
+
+
+ Part 2 done: floor, desk, and booking schemas live in OpenRegister and the app shows desks at /apps/deskdesk/.
+ NC Calendar enabled (Apps → search "Calendar" → Download). Out of the box it'll pick up new ICalendarProviders automatically — no extra wiring on your end.
+
+
+## Why not OpenConnector or a custom listener?
+
+When you first hear "sync DeskDesk bookings to Nextcloud Calendar" your instinct is probably one of:
+
+1. **Write an OpenConnector flow** that listens to `BookingCreatedEvent` and POSTs to the CalDAV endpoint
+2. **Subclass `IEventListener`** in PHP and create a CalDAV event by hand
+3. **Have the frontend `POST /remote.php/dav/...`** when the user clicks Save
+
+All three work. All three are wrong for this case, because they treat NC Calendar as a *destination* you push to. OpenRegister already exposes every schema as a calendar — you're not pushing anywhere; you're declaring that the booking schema *is* a calendar source.
+
+The same pattern applies to:
+
+| Integration | Schema annotation | What you get |
+|---|---|---|
+| **NC Calendar** | `calendarProvider` block | Read-only virtual calendar per schema |
+| **NC Activity** | (automatic, free) | CRUD events in the activity stream + dashboard widget + email digest |
+| **NC Mail Smart Picker** | `linkedTypes: ["email"]` | Paste a desk URL into NC Mail and get a rich preview |
+| **NC Contacts** | `linkedTypes: ["contact"]` | `/api/contacts/match` returns matching desk users |
+
+Anti-pattern: hand-coding `CalendarSync.vue`, `NotesService`, `MailSidebar.vue`. Use the providers.
+
+## Step 1: Add the calendarProvider block
+
+Open `lib/Settings/deskdesk_register.json` and add a `configuration` block to the `booking` schema:
+
+```json title="lib/Settings/deskdesk_register.json (excerpt) — booking schema"
+"booking": {
+ "slug": "booking",
+ "title": "Booking",
+ /* ... existing properties ... */
+
+ "configuration": {
+ "calendarProvider": {
+ "enabled": true,
+ "dtstart": "start",
+ "dtend": "end",
+ "titleTemplate": "Desk {desk} · {user}",
+ "color": "#003a8c"
+ }
+ }
+}
+```
+
+The five fields:
+
+- `enabled` — `true` exposes the schema as a virtual calendar.
+- `dtstart` — name of the property that holds the event start. We use the `start` property already on every booking.
+- `dtend` — same, but for the end. Optional — when missing, OpenRegister falls back to a sensible default (next-hour for date-time, end-of-day for date-only).
+- `titleTemplate` — string template for the event summary. Tokens in `{braces}` are resolved against the object's properties. We use `Desk {desk} · {user}`. To resolve relations (the `desk` property is a uuid pointing at a `desk` object), include `_extend=relations` when the calendar provider fetches — see the OpenRegister side.
+- `color` — hex colour of the calendar. We use cobalt-blue (`--c-blue-cobalt`).
+
+That's the whole change. No other file needs to be touched.
+
+## Step 2: Re-import the configuration
+
+Schemas in OpenRegister have a version. Bumping the schema version + re-running the import is the canonical way to update them in place. Bump the file's `info.version` (`0.2.0` → `0.3.0`) and the booking schema's `version`:
+
+```json
+"info": { "version": "0.3.0" },
+"booking": { "version": "0.3.0", /* ... */ }
+```
+
+Then trigger the import the same way as Part 2:
+
+```bash
+docker exec nextcloud php occ app:disable deskdesk
+docker exec nextcloud php occ app:enable deskdesk
+```
+
+(Or hit `POST /api/settings/load` if you've wired admin settings yet.)
+
+## Step 3: Verify in NC Calendar
+
+Open `/apps/calendar/`. You should see a new entry in the calendar list:
+
+- **Booking** — cobalt-blue colour, read-only badge, owned by your user
+
+Click it. The seed bookings from Part 2 (the Monday focus-work session, the Tuesday sprint-planning meeting, the tentative quiet morning) appear as events. Click one. The popover shows the desk + user title + the start/end times.
+
+If you don't see the calendar:
+
+- Confirm OpenRegister registered its provider: `docker exec nextcloud php occ config:list | grep openregister`
+- Make sure your `booking` schema has `configuration.calendarProvider.enabled: true` after the re-import
+- Check `nextcloud.log` for `RegisterCalendarProvider` messages
+
+## How it works under the hood
+
+OpenRegister's `RegisterCalendarProvider` implements Nextcloud's `ICalendarProvider` interface. On every Calendar API request, it:
+
+1. Lists every schema with `configuration.calendarProvider.enabled === true`
+2. For each, returns a `RegisterCalendar` (a lightweight `ICalendar` implementation)
+3. When the user opens that calendar, queries OpenRegister for objects where `dtstart` is in the requested range
+4. Transforms each object into an iCalendar VEVENT via `CalendarEventTransformer`, applying the `titleTemplate`
+
+Per-object ACLs gate event content. The provider bypasses multi-tenancy when listing schemas (so cross-tenant calendars work), but every individual event is still gated by RBAC.
+
+The same pattern is followed by [the spec](https://github.com/ConductionNL/openregister/blob/main/openspec/specs/calendar-provider/spec.md).
+
+## What you've now bought yourself
+
+Activity (`NC Activity`) is already wired. Browse `/apps/activity/` and you'll see your booking creates / updates. The dashboard activity widget + email digest both show DeskDesk events. Zero code on your end.
+
+Mail Smart Picker too: in NC Mail, paste a desk URL (`/apps/deskdesk/desks/desk-3-east-12`) and the picker resolves it into a rich preview card with the desk label + zone + equipment.
+
+You've effectively integrated DeskDesk into four Nextcloud apps with one JSON block. That's the schema-driven discipline paying off.
+
+## Troubleshooting
+
+
+ Confirm the booking schema's configuration.calendarProvider.enabled is true after the re-import. Browse /index.php/apps/openregister/api/schemas/booking and look for the configuration block in the response. If it's missing, the re-import didn't pick up the change — bump the version and try again.
+
+
+
+ The titleTemplate doesn't resolve relations by default. Add a _extend=relations via the schema's calendarProvider extras, or use the desk's slug instead of its uuid in the template ("titleTemplate": "Desk {deskSlug}" when you've stored the slug as a property).
+
+
+
+ Check the timezone on the booking's start / end values. They must be ISO 8601 with timezone info (e.g. 2026-05-12T09:00:00+02:00). Naive datetimes are interpreted as UTC and may render shifted.
+
+
+
+ The provider needs OpenRegister 0.2.x or newer. Older versions don't ship RegisterCalendarProvider. Upgrade OpenRegister via the app store.
+
+
+## What's next
+
+
+
+
+
+
diff --git a/academy/2026-05-17-deskdesk-tutorial-4-knowledge-and-ship/index.mdx b/academy/2026-05-17-deskdesk-tutorial-4-knowledge-and-ship/index.mdx
new file mode 100644
index 000000000..be21af33e
--- /dev/null
+++ b/academy/2026-05-17-deskdesk-tutorial-4-knowledge-and-ship/index.mdx
@@ -0,0 +1,443 @@
+---
+slug: deskdesk-tutorial-4-knowledge-and-ship
+title: "Build a Nextcloud app on the Conduction stack — Part 4: Knowledge + ship"
+contentType: tutorial
+authors: [conduction]
+date: 2026-05-17
+summary: Spin up xWiki locally, author zone-specific knowledge articles, surface them per-desk via an OpenRegister knowledge_article schema, package the app, publish to the Conduction app store.
+tags: [App development, OpenConnector, xWiki, Knowledge management, Release]
+apps: [openconnector, openregister]
+durationMinutes: 35
+series: deskdesk-tutorial
+partNumber: 4
+---
+
+import {
+ Outcomes,
+ Outcome,
+ Prerequisites,
+ PrerequisiteItem,
+ NextSteps,
+ NextStep,
+ HexCard,
+} from '@conduction/docusaurus-preset/components';
+
+The final part of the four-part DeskDesk tutorial. Part 3 made bookings appear in NC Calendar with one schema annotation. Part 4 brings external knowledge — zone-specific etiquette, equipment notes, troubleshooting — from xWiki into the desk detail sidebar via a `knowledge_article` OpenRegister schema. Then we package the app and ship it.
+
+External knowledge integration is the canonical case for OpenConnector: pull data from a system you don't own (xWiki here, but the same applies to Confluence, Notion, SharePoint, Decos, Mendix), surface it inside Nextcloud-native UI without making the user context-switch.
+
+{/* truncate */}
+
+
+ A running xWiki instance on http://localhost:8086, on the same Docker network as Nextcloud, sharing the existing Postgres.
+ Three xWiki articles you author yourself, scoped by zone (DeskDeskKnowledge.East.Etiquette, .Central.MeetingDesks, .West.QuietRules).
+ A new knowledge_article OpenRegister schema in DeskDesk, with seed data matching what xWiki holds.
+ A "Knowledge" tab on every desk's detail page that lists the articles whose zone matches the desk's zone, without leaving DeskDesk.
+ A versioned, packaged DeskDesk release on the Conduction app store, installable from any Nextcloud's app browser.
+
+
+
+ Parts 1–3 done: DeskDesk shell, schemas + manifest, calendar integration all working.
+ A local Nextcloud + OpenRegister stack you can docker exec into. The reference stack at Nextcloud lokaal draaien met Docker is what we test against; it already ships the xWiki service we'll start in Step 0.
+ OpenConnector installed and enabled, for when you switch from seed data to a live xWiki sync later in the tutorial.
+
+
+## Step 0: Spin up xWiki
+
+The Conduction dev stack already declares an xWiki service under the `xwiki` profile of `apps-extra/openregister/docker-compose.yml`:
+
+```yaml title="openregister/docker-compose.yml (excerpt)"
+ 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 shares the Postgres container with Nextcloud + OpenRegister, but uses a separate database called `xwiki`. Create it once:
+
+```bash
+docker exec openregister-postgres psql -U nextcloud -d postgres -c "CREATE DATABASE xwiki;"
+```
+
+Now start xWiki with the profile:
+
+```bash
+cd apps-extra/openregister
+docker compose --profile xwiki up -d xwiki
+```
+
+The first boot takes about a minute — xWiki initialises its schema in the empty Postgres database and unpacks bundled webapps. Wait for the health check to flip to healthy:
+
+```bash
+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 redirects to its **Distribution Wizard** because the empty database has no admin user and no flavor.
+
+## Step 0a: Walk the Distribution Wizard
+
+1. **Continue** past the welcome screen.
+2. **Step 1 - Admin user.** Fill in:
+ ```
+ First Name: Desk
+ Last Name: Admin
+ Username: admin
+ Password: admin1234 (any 8+ chars works)
+ Confirm: admin1234
+ Email: admin@deskdesk.local
+ ```
+ Click **Register and login**. You're now signed in as `admin`.
+3. **Step 2 - Flavor.** Click **Let the wiki be empty** (the bundled Standard Flavor is a ~150 MB download we don't need for this tutorial). Confirm.
+4. **Step 5 - Report** lists the pages created (just `Home` and `XWiki`). Click **Continue** to land on the Main page.
+
+The wiki is now bootstrapped. Sanity-check the REST endpoint:
+
+```bash
+curl -u admin:admin1234 -H 'Accept: application/json' \
+ http://localhost:8086/rest/wikis/xwiki/spaces | head -c 200
+# {"links":[],"spaces":[{"links":[...,"id":"xwiki:XWiki", ...}]}
+```
+
+Two things to note about the URL: the REST API lives at `/rest`, **not** `/xwiki/rest` (the latter is the human view path), and basic auth is the default.
+
+## Step 1: Author the knowledge articles
+
+For tutorial purposes we use a dedicated parent space **DeskDeskKnowledge** with one subspace per zone. The folder layout matches the desk's `zone` enum (`east`, `central`, `west`, `north`, `south`) so each desk maps cleanly to its zone's articles.
+
+You can author the articles via xWiki's UI (Create Page → set parent space → write content). Faster path: drive xWiki's REST API directly. The same payload shape works from cURL, OpenConnector, or any HTTP client.
+
+```bash title="Three short articles, one PUT each"
+put() {
+ curl -s -u admin:admin1234 -X PUT \
+ -H "Content-Type: application/xml" \
+ -d "$3xwiki/2.1$4" \
+ "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."
+```
+
+Each call returns `201`. Browse to `http://localhost:8086/xwiki/bin/view/DeskDeskKnowledge/East/Etiquette` to see the East article in xWiki's standard view. The same content is also fetchable via REST:
+
+```bash
+curl -u admin:admin1234 \
+ http://localhost:8086/rest/wikis/xwiki/spaces/DeskDeskKnowledge/spaces/East/pages
+```
+
+Three short articles. Realistic enough that you'd actually write them in production. The key piece is the **subspace name**: that's what we'll pivot on from DeskDesk's side.
+
+Why the tutorial walks you through authoring instead of seeding programmatically: in real teams, the wiki articles are *organic* — written by whoever notices the etiquette gap, edited by the next person, archived when the floorplan changes. The integration we build makes them findable from where the user already is, not the other way around.
+
+## Step 2: Add a knowledge_article schema to DeskDesk
+
+Before we sync from xWiki, give OpenRegister a place to put the articles. Add a fourth schema to `lib/Settings/deskdesk_register.json`:
+
+```json title="lib/Settings/deskdesk_register.json (excerpt)"
+"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" }
+ }
+}
+```
+
+Important: the **JSON key** and the **`slug` field must match** (`knowledge_article` on both sides). OpenRegister builds the REST endpoint from the slug, so a mismatch yields `Schema not found: 'knowledge_article'` at fetch time.
+
+While you're in there, bump the register's `info.version` to `0.3.0` so the import handler picks the new schema up on next run:
+
+```json title="lib/Settings/deskdesk_register.json (info block)"
+"info": {
+ "title": "DeskDesk Register",
+ "description": "Floors, desks, bookings, and knowledge articles.",
+ "version": "0.3.0"
+}
+```
+
+Also seed three articles matching the xWiki content. The seeds make the app demo-able before you wire the sync, and serve as a reference shape for the OpenConnector mapping you'll build in Step 3.
+
+```json title="components.objects (excerpt)"
+"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"
+}
+```
+
+Add the `knowledge_article` slug to the small list `SettingsService::SCHEMA_SLUGS` in [lib/Service/SettingsService.php](https://github.com/ConductionNL/deskdesk/blob/main/lib/Service/SettingsService.php) so `/api/settings` exposes its numeric id alongside the other schemas. Same in `src/store/store.js`:
+
+```js title="src/store/store.js"
+const SCHEMAS = ['floor', 'desk', 'booking', 'knowledge_article']
+```
+
+Reload the configuration so OpenRegister picks up the new schema + seeds:
+
+```bash
+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)
+```
+
+## Step 3: Create an OpenConnector source for xWiki
+
+The seeds get the UI working immediately. For *live* data — when an article changes in xWiki you want to see the change in DeskDesk on the next sync — you wire OpenConnector.
+
+Go to `/apps/openconnector/sources` and create a new 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
+```
+
+The Test should return the East article. OpenConnector saves the source with a numeric id; we reference it from the synchronisation in the next step.
+
+## Step 4: Define the synchronisation
+
+Sources surface raw API responses; **synchronisations** map them into OpenRegister objects. Create a synchronisation:
+
+```
+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
+```
+
+Save and run once. You'll see three `knowledge_article` objects in OpenRegister whose `externalId` field starts with `xwiki:`. Delete the seed objects you added in Step 2 — they're superseded by the synced ones.
+
+## Step 5: Surface articles in CnObjectSidebar
+
+The desk-detail page already mounts CnObjectSidebar. To add a **Knowledge tab** on it, declare a custom tab in the manifest:
+
+```json title="src/manifest.json (excerpt)"
+{
+ "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" }] }
+ ]
+ }
+ }
+}
+```
+
+The `data` and `metadata` tabs use built-in widget types. The `knowledge` tab points at a custom component `knowledge-tab`, which the App registers via the `customComponents` map.
+
+Create the tab component itself at `src/views/KnowledgeTab.vue`:
+
+```vue title="src/views/KnowledgeTab.vue"
+
+
+
+
+
+```
+
+Register the component in `src/App.vue` and pass it to `CnAppRoot` via the `:custom-components` prop:
+
+```vue title="src/App.vue (excerpt)"
+
+
+
+
+
+
+
+
+
+```
+
+(See the [reference repo](https://github.com/ConductionNL/deskdesk/blob/main/src/App.vue) for the complete file — App.vue also provides `objectSidebarState` so CnDetailPage's external-sidebar channel works.)
+
+Build, reload, navigate to a 3-East desk's detail URL. The right sidebar shows three tabs — Data, Knowledge, Metadata. Click **Knowledge**. The "East zone etiquette" article appears, with the body rendered and a link back to the wiki for editing.
+
+Navigate to a 2-West desk: same tab, different article ("West zone quiet rules"). The zone filter does the matching for free; you didn't write per-desk wiring.
+
+## Step 6: Package the app
+
+The release process is mechanical:
+
+1. **Bump the version** in `appinfo/info.xml` (`0.1.0` → `0.4.0`).
+2. **Run the production build:**
+ ```bash
+ composer install --no-dev --optimize-autoloader
+ npm install --legacy-peer-deps
+ npm run build
+ ```
+3. **Strip unwanted files** — vendor dev dependencies, node_modules, tests, openspec changes, .git, .github (your CI is already at the source level). The template ships a `Makefile` target for this; if not, the production tarball pattern is:
+ ```bash
+ tar --exclude=node_modules --exclude=vendor/bin \
+ --exclude=tests --exclude=openspec/changes \
+ --exclude=.git --exclude=.github \
+ -czf deskdesk-0.4.0.tar.gz deskdesk/
+ ```
+4. **Sign the tarball** with the EC key Nextcloud's app store expects (see [the Nextcloud docs](https://nextcloudappstore.readthedocs.io/en/latest/developer.html#publishing-your-app)).
+5. **Push to GitHub** with a tag matching the version (`git tag v0.4.0 && git push origin v0.4.0`). The template ships a `.github/workflows/release.yml` that picks up the tag, builds the tarball, signs it, and publishes the release.
+
+For the **Conduction app store** (`apps.conduction.nl`), the same release pipeline pushes to that endpoint when the secret is configured. See [the .github/workflows/release.yml](https://github.com/ConductionNL/nextcloud-app-template/blob/main/.github/workflows/release.yml) reference.
+
+## Step 7: Submit to apps.nextcloud.com (optional)
+
+If you want DeskDesk in the public Nextcloud app store, register at [apps.nextcloud.com](https://apps.nextcloud.com), upload your signed tarball, fill out the form (description, screenshots, license — EUPL-1.2 is fine — categories `organization`), and wait for review. The cycle is usually 1–3 days for first-time apps.
+
+## What you've built
+
+You started Part 1 with an empty template. Forty-eight steps later, you have:
+
+- A real Nextcloud app with the canonical chassis
+- Four OpenRegister schemas with relations and seed data
+- A manifest-driven shell with two small wrappers (Index, Detail) that go away once the library wires auto-fetch into the default page types
+- Bookings that appear in NC Calendar via a schema annotation
+- Knowledge articles from xWiki that appear in the desk-detail sidebar via OpenConnector
+- A versioned, packaged, publishable release
+
+Total Vue you wrote: **three small files** (`IndexPageWrapper`, `DetailPageWrapper`, `KnowledgeTab`) — about 100 lines together. Total PHP: the rename ritual + a path-resolution fix in SettingsService + the schema-slug list. Everything else is JSON.
+
+That ratio — JSON describing what you want, framework rendering it — is the entire point of the Conduction stack. Schemas drive both backend and frontend. Manifests drive the shell. Schema annotations drive cross-app integrations. You declare; the framework renders.
+
+## Troubleshooting
+
+
+ The schema's JSON key and the slug field must match. If you used knowledge-article (hyphen) for one and knowledge_article (underscore) for the other, OpenRegister's URL lookup fails. Pick one, use it both places.
+
+
+
+ CnPageRenderer forwards page.config and $route.params as raw props. The route param is id but CnDetailPage's prop is objectId. The DetailPageWrapper bridges the names + fetches the object via the store; without the wrapper, CnDetailPage just renders the empty state.
+
+
+
+ Pick Let the wiki be empty at Step 2. The empty flavor still supports REST + page creation, which is all the tutorial needs.
+
+
+
+ Set Accept: application/json in the source's Headers section. xWiki defaults to XML for REST responses, but content-negotiates JSON when asked.
+
+
+## Where to next
+
+
+
+
+
+
diff --git a/docusaurus.config.js b/docusaurus.config.js
index 5cf06753b..ae1b9bc7f 100644
--- a/docusaurus.config.js
+++ b/docusaurus.config.js
@@ -20,7 +20,15 @@ const {createConfig} = require('@conduction/docusaurus-preset');
module.exports = createConfig({
title: 'Conduction',
tagline: 'Open-source apps voor de Nextcloud-werkplek.',
- url: 'https://conduction.nl',
+ /* Must match static/CNAME (www.conduction.nl) — GitHub Pages serves
+ only on the CNAME host and 301-redirects the apex to it. Using the
+ bare apex here made every canonical/og:url/sitemap entry point at
+ conduction.nl, adding an apex→www hop on top of whatever the
+ Cloudflare vanity worker already does — a redirect-loop amplifier
+ on connext./commonground. (trailingSlash is true, set by the
+ preset, so the worker must target https://www.conduction.nl/connext/
+ — with the www and the trailing slash — not /connext.) */
+ url: 'https://www.conduction.nl',
baseUrl: '/',
organizationName: 'ConductionNL',
diff --git a/package-lock.json b/package-lock.json
index 2a24a7857..a70924d26 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,8 +7,9 @@
"": {
"name": "conduction-website",
"version": "0.0.0",
+ "hasInstallScript": true,
"dependencies": {
- "@conduction/docusaurus-preset": "^1.7.0",
+ "@conduction/docusaurus-preset": "^3.3.0",
"@docusaurus/core": "^3.5.0",
"@docusaurus/preset-classic": "^3.5.0",
"@mdx-js/react": "^3.0.0",
@@ -1987,9 +1988,9 @@
}
},
"node_modules/@conduction/docusaurus-preset": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/@conduction/docusaurus-preset/-/docusaurus-preset-1.7.0.tgz",
- "integrity": "sha512-JH7aXaRGZCvkHZTLTLRh3gDWhj/2zAc8S4eBGS67hCfDIwzP5SK+Uj5BidoqbSi0FEEnekEoya2AVVLXbNv/OQ==",
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/@conduction/docusaurus-preset/-/docusaurus-preset-3.3.0.tgz",
+ "integrity": "sha512-xpNp/Azxwp1TbvnGLN8egxpPk/ZlpUEzdEHfwUDxf7McfpsM4C16JdgXaQ33n6GZAMCHKWrbseKjfSwRSbrmtQ==",
"license": "EUPL-1.2",
"peerDependencies": {
"@docusaurus/core": "^3.0.0",
@@ -2209,19 +2210,6 @@
"postcss": "^8.4"
}
},
- "node_modules/@csstools/postcss-cascade-layers/node_modules/postcss-selector-parser": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
- "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
- "license": "MIT",
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/@csstools/postcss-color-function": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.12.tgz",
@@ -2608,19 +2596,6 @@
"postcss": "^8.4"
}
},
- "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
- "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
- "license": "MIT",
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/@csstools/postcss-light-dark-function": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.11.tgz",
@@ -3055,19 +3030,6 @@
"postcss": "^8.4"
}
},
- "node_modules/@csstools/postcss-scope-pseudo-class/node_modules/postcss-selector-parser": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
- "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
- "license": "MIT",
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/@csstools/postcss-sign-functions": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.4.tgz",
@@ -7309,19 +7271,6 @@
"postcss": "^8.4"
}
},
- "node_modules/css-blank-pseudo/node_modules/postcss-selector-parser": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
- "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
- "license": "MIT",
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/css-declaration-sorter": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.4.0.tgz",
@@ -7361,19 +7310,6 @@
"postcss": "^8.4"
}
},
- "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
- "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
- "license": "MIT",
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/css-loader": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz",
@@ -13702,19 +13638,6 @@
"postcss": "^8.4"
}
},
- "node_modules/postcss-attribute-case-insensitive/node_modules/postcss-selector-parser": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
- "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
- "license": "MIT",
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/postcss-calc": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz",
@@ -13731,6 +13654,19 @@
"postcss": "^8.2.2"
}
},
+ "node_modules/postcss-calc/node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/postcss-clamp": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz",
@@ -13946,19 +13882,6 @@
"postcss": "^8.4"
}
},
- "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
- "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
- "license": "MIT",
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/postcss-dir-pseudo-class": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-9.0.1.tgz",
@@ -13984,19 +13907,6 @@
"postcss": "^8.4"
}
},
- "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
- "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
- "license": "MIT",
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/postcss-discard-comments": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz",
@@ -14060,6 +13970,19 @@
"postcss": "^8.4.31"
}
},
+ "node_modules/postcss-discard-unused/node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/postcss-double-position-gradients": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.4.tgz",
@@ -14112,19 +14035,6 @@
"postcss": "^8.4"
}
},
- "node_modules/postcss-focus-visible/node_modules/postcss-selector-parser": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
- "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
- "license": "MIT",
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/postcss-focus-within": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-9.0.1.tgz",
@@ -14150,19 +14060,6 @@
"postcss": "^8.4"
}
},
- "node_modules/postcss-focus-within/node_modules/postcss-selector-parser": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
- "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
- "license": "MIT",
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/postcss-font-variant": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz",
@@ -14346,6 +14243,19 @@
"postcss": "^8.4.31"
}
},
+ "node_modules/postcss-merge-rules/node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/postcss-minify-font-values": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz",
@@ -14410,6 +14320,19 @@
"postcss": "^8.4.31"
}
},
+ "node_modules/postcss-minify-selectors/node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/postcss-modules-extract-imports": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz",
@@ -14439,19 +14362,6 @@
"postcss": "^8.1.0"
}
},
- "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
- "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
- "license": "MIT",
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/postcss-modules-scope": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz",
@@ -14467,19 +14377,6 @@
"postcss": "^8.1.0"
}
},
- "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
- "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
- "license": "MIT",
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/postcss-modules-values": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
@@ -14522,19 +14419,6 @@
"postcss": "^8.4"
}
},
- "node_modules/postcss-nesting/node_modules/postcss-selector-parser": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
- "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
- "license": "MIT",
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/postcss-normalize-charset": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz",
@@ -14885,19 +14769,6 @@
"postcss": "^8.4"
}
},
- "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
- "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
- "license": "MIT",
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/postcss-reduce-idents": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-6.0.3.tgz",
@@ -14978,7 +14849,7 @@
"postcss": "^8.4"
}
},
- "node_modules/postcss-selector-not/node_modules/postcss-selector-parser": {
+ "node_modules/postcss-selector-parser": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
@@ -14991,19 +14862,6 @@
"node": ">=4"
}
},
- "node_modules/postcss-selector-parser": {
- "version": "6.1.2",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
- "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
- "license": "MIT",
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/postcss-sort-media-queries": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/postcss-sort-media-queries/-/postcss-sort-media-queries-5.2.0.tgz",
@@ -15050,6 +14908,19 @@
"postcss": "^8.4.31"
}
},
+ "node_modules/postcss-unique-selectors/node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
@@ -16152,6 +16023,13 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/search-insights": {
+ "version": "2.17.3",
+ "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz",
+ "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/section-matter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
@@ -16911,6 +16789,19 @@
"postcss": "^8.4.31"
}
},
+ "node_modules/stylehacks/node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
diff --git a/package.json b/package.json
index 8022a0e75..7ca89dfe8 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,10 @@
"description": "conduction.nl, the Conduction company hub. Hosts apps, solutions, partners, plus ConNext and Common Ground vanity landings.",
"scripts": {
"docusaurus": "docusaurus",
+ "postinstall": "node scripts/sync-app-downloads.js",
+ "prestart": "node scripts/sync-app-downloads.js",
"start": "docusaurus start",
+ "prebuild": "python3 .github/scripts/app_downloads.py && node scripts/sync-app-downloads.js",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
@@ -16,7 +19,7 @@
"ci": "npm ci --legacy-peer-deps && npm run build"
},
"dependencies": {
- "@conduction/docusaurus-preset": "^1.7.0",
+ "@conduction/docusaurus-preset": "^3.3.0",
"@docusaurus/core": "^3.5.0",
"@docusaurus/preset-classic": "^3.5.0",
"@mdx-js/react": "^3.0.0",
diff --git a/scripts/sync-app-downloads.js b/scripts/sync-app-downloads.js
new file mode 100644
index 000000000..3b59d9ab2
--- /dev/null
+++ b/scripts/sync-app-downloads.js
@@ -0,0 +1,32 @@
+#!/usr/bin/env node
+/**
+ * Mirrors data/app-downloads.json into node_modules/@conduction/data/ so
+ * @conduction/docusaurus-preset's stats import resolves on a freshly-
+ * installed tree. The preset's src/data/app-downloads.js requires the
+ * file at `../../../data/app-downloads.json` relative to its own
+ * location, which targets node_modules/@conduction/data/ — a path the
+ * npm install layout does not create on its own.
+ *
+ * Hooked into postinstall + prestart + prebuild so the file is in place
+ * for both dev and prod builds. Safe to re-run; idempotent on contents.
+ */
+const fs = require('fs');
+const path = require('path');
+
+const SRC = path.join(__dirname, '..', 'data', 'app-downloads.json');
+const DST_DIR = path.join(__dirname, '..', 'node_modules', '@conduction', 'data');
+const DST = path.join(DST_DIR, 'app-downloads.json');
+
+if (!fs.existsSync(SRC)) {
+ console.warn(`[sync-app-downloads] source missing at ${SRC}; skipping`);
+ process.exit(0);
+}
+
+try {
+ fs.mkdirSync(DST_DIR, { recursive: true });
+ fs.copyFileSync(SRC, DST);
+ console.log(`[sync-app-downloads] copied ${SRC} → ${DST}`);
+} catch (err) {
+ console.warn(`[sync-app-downloads] copy failed: ${err.message}`);
+ process.exit(0);
+}
diff --git a/src/data/apps-catalog.js b/src/data/apps-catalog.js
index b05f6ac96..a048a51f4 100644
--- a/src/data/apps-catalog.js
+++ b/src/data/apps-catalog.js
@@ -68,13 +68,6 @@ const PRESENTATION = {
categories: ['Dashboards'],
icon: ,
},
- openwoo: {
- name: 'OpenWoo',
- tagline: 'WOO-compliant publication flow. Active disclosure, queryable archive, citation-stable URLs.',
- href: '/apps/openwoo',
- categories: ['Processes'],
- icon: ,
- },
zaakafhandelapp: {
name: 'ZaakAfhandelApp',
tagline: 'Citizen-facing case-status portal. ZGW APIs, archief koppelvlakken, audit trail.',
@@ -117,6 +110,13 @@ const PRESENTATION = {
categories: ['Processes'],
icon: ,
},
+ scholiq: {
+ name: 'Scholiq',
+ tagline: 'Learning record + LMS. Cursussen, inschrijvingen, certificaten, compliance-training.',
+ href: '/apps/scholiq',
+ categories: ['Processes'],
+ icon: ,
+ },
nldesign: {
name: 'NLDesign',
tagline: 'Drop-in NLDS theme for Nextcloud, with the Conduction component variants on top.',
@@ -124,11 +124,50 @@ const PRESENTATION = {
categories: ['Documents'],
icon: ,
},
+ shillinq: {
+ name: 'Shillinq',
+ tagline: 'Business administration on Nextcloud. Invoices, contracts, procurement on shared registers.',
+ href: '/apps/shillinq',
+ categories: ['Processes'],
+ icon: ,
+ },
+ openbuilt: {
+ name: 'OpenBuilt',
+ tagline: 'Citizen-developer app builder. Compose Nextcloud apps from registers, connectors, workflows, no PHP.',
+ href: '/apps/openbuilt',
+ categories: ['Processes'],
+ icon: ,
+ },
+ doriath: {
+ name: 'Doriath',
+ tagline: 'Self-hosted password and secrets vault. Per-user, per-team, audited. In development.',
+ href: '/apps/doriath',
+ categories: ['Connectors'],
+ icon: ,
+ },
+ 'app-versions': {
+ name: 'App Versions',
+ tagline: 'Pin and roll back any Nextcloud app version. Multi-source picker, audit-trailed. In development.',
+ href: '/apps/app-versions',
+ categories: ['Data'],
+ icon: ,
+ },
};
/* Categories the website filters by, in the order the chips render. */
export const APP_CATEGORIES = ['All', 'Data', 'Processes', 'Connectors', 'Documents', 'AI', 'Dashboards'];
+/* Site-wide app-count constants — the single source of truth for "we ship N apps".
+ APP_COUNT is the size of the PRESENTATION map (every Conduction app, including
+ in-development ones). Use this in copy instead of hardcoding a digit or word,
+ so the headline number stays in sync with the registry. INTEGRATED_COUNT is the
+ number of third-party tools we surface in the External-tools shelf on /connext;
+ ECOSYSTEM_COUNT is the headline "apps in the diagram" total used as the page
+ title. Update INTEGRATED_COUNT whenever the shelf changes. */
+export const APP_COUNT = Object.keys(PRESENTATION).length;
+export const INTEGRATED_COUNT = 14;
+export const ECOSYSTEM_COUNT = APP_COUNT + INTEGRATED_COUNT;
+
/* Pick a status label from the version string. The Nextcloud app
store leaves the latest_version as-is, so "0.7.9-beta.8" reads as
beta and "1.6.0" reads as stable. Falls back to GitHub's tag when
@@ -192,7 +231,7 @@ export function getApps() {
return out;
}
-/* Convenience: the totals strip ("12 apps in the store · 22,400 installs"). */
+/* Convenience: the totals strip ("N apps in the store · M installs"). */
export function getCatalogTotals() {
const apps = getApps();
const installs = apps.reduce((n, a) => n + (a.downloads || 0), 0);
diff --git a/src/data/partners-catalog.js b/src/data/partners-catalog.js
index 3e6285084..7ab3cd025 100644
--- a/src/data/partners-catalog.js
+++ b/src/data/partners-catalog.js
@@ -9,9 +9,23 @@
* apps. Add href when a /partners/ detail page exists. Add
* logo when we have permission to display it.
*
- * tier values: 'partner' | 'certified' | 'strategic'.
+ * Tier slugs (low → high):
+ * - host Hosts our open-source apps for their customers. May
+ * resell Nextcloud-supported variants through their
+ * own channels. No SLA with Conduction. No direct line
+ * to our support, no input on the roadmap, not eligible
+ * to respond to public tenders with our products.
+ * - service Has an SLA with Conduction. Their team can call us
+ * directly for third-line support, named contacts,
+ * input on bug priority and feature requests.
+ * - certified Trained and certified by Conduction. Joint roadmap.
+ * Eligible to respond to public tenders alongside us.
+ *
* apps: free-form list of app or service names the partner
* ships, used as a facet on /partners.
+ * solutions: free-form list of solution slugs (matching
+ * /solutions/) the partner delivers, used both as
+ * a facet on /partners and for the reverse lookup below.
*
* Summaries stay in Dutch when the partner's market is Dutch — the
* audience that filters by an MKB hosting partner is the same audience
@@ -22,96 +36,107 @@
import React from 'react';
export const PARTNERS = [
+ // --------------------------------------------------------------
+ // Certified — top tier, joint roadmap, tender-eligible
+ // --------------------------------------------------------------
{
href: '/partners/acato',
- tier: 'strategic',
+ tier: 'certified',
name: 'Acato',
- logo: '/img/partners/acato.png',
- summary: <>Nederlandse open-source specialist uit Almere. Bouwt en host Nextcloud-omgevingen voor gemeenten en zorginstellingen, met focus op AVG-compliance en NLDS. Strategisch partner op hosting, stack-implementatie en gezamenlijke roadmap.>,
- apps: ['OpenRegister', 'OpenCatalogi', 'DocuDesk'],
- solutions: ['woo', 'archief', 'software-catalog'],
+ logo: '/img/partners/acato.svg',
+ summary: <>Digital agency uit Almere. Bouwt toegankelijke websites en webapplicaties voor gemeenten en overheidsorganisaties, met focus op WCAG 2.2 AA en NLDS. Levert de Conduction Woo-solution op OpenCatalogi, OpenRegister en OpenConnector.>,
+ apps: ['OpenCatalogi', 'OpenRegister', 'OpenConnector'],
+ solutions: ['woo'],
},
+
+ // --------------------------------------------------------------
+ // Service — SLA with Conduction, third-line support
+ // --------------------------------------------------------------
{
- tier: 'certified',
+ tier: 'service',
+ name: 'Shift2',
+ logo: '/img/partners/shift2.png',
+ summary: <>Nederlandse softwarebouwer voor gemeenten, waterschappen en provincies, onderdeel van de Conxillium-groep. Levert CMS, burgerzaken en formulieren, en daarnaast de Conduction Woo-solution op OpenCatalogi, OpenRegister en OpenConnector.>,
+ apps: ['OpenCatalogi', 'OpenRegister', 'OpenConnector'],
+ solutions: ['woo'],
+ },
+
+ // --------------------------------------------------------------
+ // Host — ships our apps, no SLA with Conduction
+ // --------------------------------------------------------------
+ {
+ tier: 'host',
name: 'Centric',
logo: '/img/partners/centric.png',
- summary: <>Eén van Nederlands grootste software-leveranciers voor de publieke sector. Officieel Nextcloud-distributeur. Centric-klanten krijgen Conduction-apps geïntegreerd in hun bestaande Centric-omgeving.>,
- apps: ['Nextcloud', 'OpenZaak', 'OpenRegister', 'DocuDesk'],
- solutions: ['zaakafhandeling', 'archief'],
+ summary: <>Een van Nederlands grootste IT-leveranciers voor gemeenten en de publieke sector. Werkt samen met Nextcloud aan soevereine werkplekken en host DocuDesk binnen dat traject.>,
+ apps: ['Nextcloud', 'DocuDesk'],
+ solutions: [],
},
{
href: '/partners/procolix',
- tier: 'certified',
+ tier: 'host',
name: 'Procolix',
logo: '/img/partners/procolix.png',
- summary: <>Nederlandse hoster en leverancier van proces- en zaakmanagement-software. Officieel Nextcloud-distributeur. Certified op DocuDesk en OpenConnector voor procesintegraties bij gemeenten en uitvoeringsorganisaties.>,
- apps: ['Nextcloud', 'DocuDesk', 'OpenConnector'],
- solutions: ['zaakafhandeling', 'archief', 'legacy-erp'],
+ summary: <>Nederlandse managed-Nextcloud-hoster uit Dordrecht. Levert beheerde Nextcloud-omgevingen op eigen EU-infrastructuur, open-source als uitgangspunt.>,
+ apps: ['Nextcloud'],
+ solutions: [],
},
{
- tier: 'certified',
+ tier: 'host',
name: 'The Goodcloud',
logo: '/img/partners/goodcloud.png',
- summary: <>Nederlandse Nextcloud-hoster voor MKB en non-profits. Officieel Nextcloud-distributeur. Levert beheerde omgevingen met Conduction-apps pre-installed, privacy-eerst hosting in NL.>,
- apps: ['Nextcloud', 'OpenRegister', 'MyDash'],
- solutions: ['mkb-workspace'],
+ summary: <>Nederlandse managed-Nextcloud-hoster. Privacy-eerst, geen tracking, data in Nederland. Voor MKB, non-profits en privacybewuste organisaties.>,
+ apps: ['Nextcloud'],
+ solutions: [],
},
{
- tier: 'partner',
+ tier: 'host',
name: 'BCT',
logo: '/img/partners/bct.png',
- summary: <>Nederlandse leverancier van document- en zaaksysteem-software voor de overheid. BCT-implementaties praten via OpenConnector met de Conduction-registers.>,
- apps: ['OpenConnector', 'DocuDesk', 'OpenZaak'],
- solutions: ['zaakafhandeling', 'archief', 'legacy-erp'],
+ summary: <>Nederlandse leverancier van informatie- en documentmanagement-software (Corsa, Verix). Levert de Conduction Woo-solution aan organisaties die hun informatie-governance op orde willen krijgen.>,
+ apps: ['OpenCatalogi', 'OpenRegister', 'OpenConnector'],
+ solutions: ['woo'],
},
{
- tier: 'partner',
+ tier: 'host',
name: 'Open Gemeenten',
logo: '/img/partners/open-gemeenten.png',
- summary: <>Coöperatieve community van gemeenten die samen open-source software ontwikkelen en delen. Levert OpenZaak-implementaties bij aangesloten gemeenten en draagt patches terug naar upstream.>,
- apps: ['OpenZaak', 'OpenRegister', 'OpenCatalogi'],
- solutions: ['zaakafhandeling', 'software-catalog'],
+ summary: <>Open-source platform voor gemeentelijke websites. Bedient ruim 30 gemeenten met toegankelijke sites (WCAG, internet.nl). Levert daarnaast de Conduction Woo-solution.>,
+ apps: ['OpenCatalogi', 'OpenRegister', 'OpenConnector'],
+ solutions: ['woo'],
},
{
- tier: 'partner',
- name: 'Exxellence',
+ tier: 'host',
+ name: 'xxllnc',
logo: '/img/partners/exxellence.png',
- summary: <>Nederlandse softwarebouwer gespecialiseerd in zaakafhandeling en burger-portaal-toepassingen. Reference-deploys op OpenZaak v4.x bij meerdere middelgrote gemeenten.>,
- apps: ['OpenZaak', 'DocuDesk'],
- solutions: ['zaakafhandeling'],
+ summary: <>Nederlandse leverancier van zaakgericht-werken-software voor gemeenten (xxllnc Zaken, plus apps voor belastingen en het sociaal domein). Levert daarnaast de Conduction Woo-solution.>,
+ apps: ['OpenCatalogi', 'OpenRegister', 'OpenConnector'],
+ solutions: ['woo'],
},
{
href: '/partners/yard',
- tier: 'partner',
+ tier: 'host',
name: 'YARD',
logo: '/img/partners/yard.png',
- summary: <>Digital design- en development-bureau uit Utrecht. Ontwerpt en bouwt klantgerichte digitale producten op Nextcloud voor gemeenten en publieke organisaties.>,
- apps: ['MyDash', 'OpenCatalogi'],
- solutions: ['software-catalog', 'mkb-workspace'],
+ summary: <>Digital agency uit Utrecht voor gemeenten, zorg en kennisorganisaties. Open-source-georiënteerd, ruim 80 projecten. Levert de Conduction Woo-solution.>,
+ apps: ['OpenCatalogi', 'OpenRegister', 'OpenConnector'],
+ solutions: ['woo'],
},
{
- tier: 'partner',
+ tier: 'host',
name: 'iO',
logo: '/img/partners/io.webp',
- summary: <>Internationaal digital agency met Nederlandse vestiging in Amsterdam. Integreert Conduction-apps in bredere klantreis-trajecten voor publieke en semi-publieke organisaties.>,
- apps: ['OpenConnector', 'MyDash'],
- solutions: ['legacy-erp', 'mkb-workspace'],
- },
- {
- tier: 'partner',
- name: 'Shift2',
- logo: '/img/partners/shift2.png',
- summary: <>Nederlands development-bureau gespecialiseerd in Nextcloud en open source. Implementeert Conduction-stacks bij MKB-klanten en gemeenten in midden-Nederland.>,
- apps: ['OpenRegister', 'MyDash'],
- solutions: ['mkb-workspace'],
+ summary: <>Belgisch-Nederlands digital agency met kantoren in Amsterdam en Rotterdam. Bouwt klantgerichte digitale platforms en levert de Conduction Woo-solution aan publieke en semi-publieke organisaties.>,
+ apps: ['OpenCatalogi', 'OpenRegister', 'OpenConnector'],
+ solutions: ['woo'],
},
{
- tier: 'partner',
+ tier: 'host',
name: 'Sendent',
logo: '/img/partners/sendent.png',
- summary: <>Nederlandse leverancier die Microsoft Outlook en Teams koppelt aan Nextcloud. Levert support op MyDash voor klanten die hun e-mail- en bestandsstroom binnen de eigen Nextcloud-omgeving willen houden.>,
+ summary: <>Nederlandse leverancier die Microsoft Outlook en Teams koppelt aan Nextcloud. Levert support op MyDash voor klanten die hun mail- en bestandsstroom binnen de eigen Nextcloud-omgeving willen houden.>,
apps: ['MyDash'],
- solutions: ['mkb-workspace'],
+ solutions: [],
},
];
@@ -131,6 +156,6 @@ export const BECOME_PARTNER = {
href: '/support#become-a-partner',
eyebrow: 'Become a partner',
title: 'Ship Conduction to your customers.',
- body: 'Join as Partner, Certified, or Strategic. Conduction trains your team, you handle the rollouts. The apps stay open source, the relationship stays direct.',
+ body: 'Three tiers: Host (ship our apps), Service (SLA + third-line support), Certified (trained, joint roadmap, tender-eligible). The apps stay open source, the relationship stays direct.',
ctaLabel: 'Apply through Support',
};
diff --git a/src/data/sidecars-catalog.js b/src/data/sidecars-catalog.js
new file mode 100644
index 000000000..be1341c7f
--- /dev/null
+++ b/src/data/sidecars-catalog.js
@@ -0,0 +1,130 @@
+/**
+ * sidecars-catalog.js
+ * -------------------
+ * Hand-curated display metadata for ExApp sidecars. Mirrors the shape
+ * of apps-catalog.js but lives in its own file so the /apps grid
+ * stays focused on apps we build, not apps we wrap.
+ *
+ * Keys are the GitHub repo slug (matches `apps[].id` in
+ * `data/app-downloads.json`). URLs are mirrored in the sidecars-
+ * registry shipped from `@conduction/docusaurus-preset/data/sidecars-
+ * registry`; when you add or rename a sidecar update both.
+ *
+ * Adding a new sidecar: drop a row into PRESENTATION below plus a
+ * product page at `src/pages/sidecars/.mdx`.
+ */
+
+import React from 'react';
+import downloadsJson from '../../data/app-downloads.json';
+
+const PRESENTATION = {
+ openklant: {
+ name: 'OpenKlant',
+ tagline: 'Sidecar that wraps the OpenKlant citizen-contact register. ZGW-compatible, installed from Nextcloud.',
+ href: '/sidecars/openklant',
+ categories: ['Registers'],
+ icon: ,
+ },
+ openzaak: {
+ name: 'OpenZaak',
+ tagline: 'Sidecar for the OpenZaak case-management API. Koppelvlakken voor ZGW, vanuit Nextcloud.',
+ href: '/sidecars/openzaak',
+ categories: ['Registers'],
+ icon: ,
+ },
+ valtimo: {
+ name: 'Valtimo',
+ tagline: 'Sidecar for the Valtimo BPMN engine. Workflow orchestration alongside the registers.',
+ href: '/sidecars/valtimo',
+ categories: ['Workflow'],
+ icon: ,
+ },
+ 'n8n-nextcloud': {
+ name: 'n8n',
+ tagline: 'Sidecar for n8n. Visual workflow automation, installed as a Nextcloud app.',
+ href: '/sidecars/n8n',
+ categories: ['Workflow'],
+ icon: ,
+ },
+ keycloak: {
+ name: 'Keycloak',
+ tagline: 'Sidecar for Keycloak. Self-hosted identity and single sign-on next to Nextcloud.',
+ href: '/sidecars/keycloak',
+ categories: ['Identity'],
+ icon: ,
+ },
+ ollama: {
+ name: 'Ollama',
+ tagline: 'Sidecar for Ollama. Local LLM inference served from the Nextcloud workspace.',
+ href: '/sidecars/ollama',
+ categories: ['AI'],
+ icon: ,
+ },
+ 'open-webui': {
+ name: 'Open WebUI',
+ tagline: 'Sidecar for Open WebUI. Chat-style UI for local models, inside Nextcloud.',
+ href: '/sidecars/open-webui',
+ categories: ['AI'],
+ icon: ,
+ },
+ opentalk: {
+ name: 'OpenTalk',
+ tagline: 'Sidecar for OpenTalk. GDPR-compliant video conferencing, end-to-end encrypted, packaged as a Nextcloud ExApp.',
+ href: '/sidecars/opentalk',
+ categories: ['Workflow'],
+ icon: ,
+ },
+};
+
+export const SIDECAR_CATEGORIES = ['All', 'Registers', 'Workflow', 'Identity', 'AI'];
+
+function statusFor(record) {
+ const ver = (record.store && record.store.latest_version) || (record.github && record.github.latest_release) || '';
+ const v = ver.toLowerCase();
+ if (!ver) return 'COMING SOON';
+ if (v.includes('beta') || v.includes('alpha') || v.includes('rc')) return 'BETA';
+ if (v.startsWith('0.') || v.startsWith('v0.')) return 'BETA';
+ return 'STABLE';
+}
+
+function versionLabel(record) {
+ const raw = (record.store && record.store.latest_version) || (record.github && record.github.latest_release) || '';
+ const stripped = raw.replace(/^v/, '').replace(/-(beta|alpha|rc).*$/i, '');
+ const short = stripped.split('.').slice(0, 2).join('.');
+ const dl = record.github && record.github.downloads;
+ const dlLabel = dl && dl > 0 ? `${dl.toLocaleString('en-US')} installs` : null;
+ return [short && `v${short}`, dlLabel].filter(Boolean).join(' · ');
+}
+
+export function getSidecars() {
+ const seen = new Set();
+ const out = [];
+ for (const record of downloadsJson.apps) {
+ const meta = PRESENTATION[record.id];
+ if (!meta) continue;
+ if (seen.has(record.id)) continue;
+ seen.add(record.id);
+ out.push({
+ ...meta,
+ status: statusFor(record),
+ version: versionLabel(record),
+ downloads: (record.github && record.github.downloads) || 0,
+ });
+ }
+ for (const id of Object.keys(PRESENTATION)) {
+ if (seen.has(id)) continue;
+ out.push({
+ ...PRESENTATION[id],
+ status: 'COMING SOON',
+ version: '',
+ downloads: 0,
+ });
+ }
+ const statusRank = {STABLE: 0, BETA: 1, 'COMING SOON': 2};
+ out.sort((a, b) => {
+ const r = (statusRank[a.status] ?? 9) - (statusRank[b.status] ?? 9);
+ if (r !== 0) return r;
+ return (b.downloads || 0) - (a.downloads || 0);
+ });
+ return out;
+}
diff --git a/src/data/sidecars-registry.js b/src/data/sidecars-registry.js
new file mode 100644
index 000000000..f5fa43c9e
--- /dev/null
+++ b/src/data/sidecars-registry.js
@@ -0,0 +1,46 @@
+/**
+ * @conduction/docusaurus-preset/data/sidecars-registry
+ *
+ * URL-only registry of every public Conduction ExApp sidecar.
+ * Sidecars are Nextcloud-installable wrappers around an existing
+ * third-party service (n8n, Keycloak, Ollama, the Common Ground
+ * registers). They live next to the apps-registry, not inside it,
+ * because they are not apps we build, they are apps we wrap.
+ *
+ * The shape mirrors apps-registry so cross-link components can treat
+ * a sidecar and an app uniformly:
+ *
+ * 1. /sidecars/ product detail page
+ * 2. https://docs.conduction.nl/ documentation site
+ * 3. /academy?app= academy posts filtered by app
+ *
+ * Adding a new sidecar: add an entry here plus a product page at
+ * conduction-website/src/pages/sidecars/.mdx
+ */
+
+export const SIDECARS_REGISTRY = {
+ openklant: {slug: 'openklant', name: 'OpenKlant', productHref: '/sidecars/openklant', docsHref: 'https://docs.conduction.nl/openklant', academyHref: '/academy?app=openklant'},
+ openzaak: {slug: 'openzaak', name: 'OpenZaak', productHref: '/sidecars/openzaak', docsHref: 'https://docs.conduction.nl/openzaak', academyHref: '/academy?app=openzaak'},
+ valtimo: {slug: 'valtimo', name: 'Valtimo', productHref: '/sidecars/valtimo', docsHref: 'https://docs.conduction.nl/valtimo', academyHref: '/academy?app=valtimo'},
+ 'n8n-nextcloud': {slug: 'n8n-nextcloud', name: 'n8n', productHref: '/sidecars/n8n', docsHref: 'https://docs.conduction.nl/n8n', academyHref: '/academy?app=n8n'},
+ keycloak: {slug: 'keycloak', name: 'Keycloak', productHref: '/sidecars/keycloak', docsHref: 'https://docs.conduction.nl/keycloak', academyHref: '/academy?app=keycloak'},
+ ollama: {slug: 'ollama', name: 'Ollama', productHref: '/sidecars/ollama', docsHref: 'https://docs.conduction.nl/ollama', academyHref: '/academy?app=ollama'},
+ 'open-webui': {slug: 'open-webui', name: 'Open WebUI', productHref: '/sidecars/open-webui', docsHref: 'https://docs.conduction.nl/open-webui', academyHref: '/academy?app=open-webui'},
+};
+
+export const SIDECAR_SLUGS = Object.keys(SIDECARS_REGISTRY);
+
+export const SIDECAR_LABELS = SIDECAR_SLUGS.reduce((acc, slug) => {
+ acc[slug] = SIDECARS_REGISTRY[slug].name;
+ return acc;
+}, {});
+
+/** Resolve a slug to its sidecar registry entry; returns undefined for unknown slugs. */
+export function getSidecar(slug) {
+ return SIDECARS_REGISTRY[slug];
+}
+
+/** Resolve an array of slugs, dropping any that aren't in the registry. */
+export function getSidecars(slugs = []) {
+ return slugs.map(getSidecar).filter(Boolean);
+}
diff --git a/src/pages/about.mdx b/src/pages/about.mdx
index 59870923b..a135ab914 100644
--- a/src/pages/about.mdx
+++ b/src/pages/about.mdx
@@ -1,6 +1,6 @@
---
title: About, Conduction
-description: Conduction is a small Dutch open-source product company. Twelve apps, one workspace, one team in Amsterdam.
+description: Conduction is a small Dutch open-source product company. A workspace of open-source apps for Nextcloud, one team in Amsterdam.
hide_table_of_contents: true
---
diff --git a/src/pages/apps/app-versions.mdx b/src/pages/apps/app-versions.mdx
new file mode 100644
index 000000000..75bcd7dfe
--- /dev/null
+++ b/src/pages/apps/app-versions.mdx
@@ -0,0 +1,72 @@
+---
+title: App Versions, Conduction
+description: Pin and roll back any Nextcloud app version. Multi-source picker, audit-trailed. In development.
+hide_table_of_contents: true
+---
+
+import {DetailHero, Section, SectionHead, FeatureList, FeatureItem, CtaBanner} from '@conduction/docusaurus-preset/components';
+
+Pin and roll back any Nextcloud app version. Multi-source picker over the app store, GitHub releases, and private registries. Audit-trailed, admin-only, a safety net for fleet upgrades.>}
+ secondaryCta={{label: 'Read the docs', href: 'https://app-versions.conduction.nl/'}}
+ tertiaryCta={{label: 'View on GitHub', href: 'https://github.com/ConductionNL/app-versions'}}
+ iconColor="var(--c-blue-cobalt)"
+ icon={}
+/>
+
+
+
+
+ }
+ title="Per-app version table."
+ >
+ For every installed app: current version, available versions per source, last upgrade, rollback target. One screen, no command line.
+
+ }
+ title="Multiple sources."
+ >
+ Nextcloud app store, GitHub releases, private registries. A token per source if needed; private GitHub releases work the same as public ones.
+
+ }
+ title="Rollback or pin."
+ >
+ One click to revert to a known-good release. Or pin a version so a future auto-update can't move it. Per app, not fleet-wide.
+
+ }
+ title="Audit trail."
+ >
+ Every version change recorded: who, when, from what to what, why. Visible to the admin team, exportable for compliance.
+
+
+
+
+
+
+
+
+App Versions is the difference between 'roll forward and hope' and 'roll back if it bites'. Open-source, admin-only, no extra service to run.>}
+ primaryCta={{label: 'Read the docs', href: 'https://app-versions.conduction.nl/'}}
+ secondaryCta={{label: 'View on GitHub', href: 'https://github.com/ConductionNL/app-versions'}}
+/>
diff --git a/src/pages/apps/decidesk.mdx b/src/pages/apps/decidesk.mdx
index d3979aae9..e8f777986 100644
--- a/src/pages/apps/decidesk.mdx
+++ b/src/pages/apps/decidesk.mdx
@@ -4,7 +4,7 @@ description: Decision-support and board management on Nextcloud. Agenda, dossier
hide_table_of_contents: true
---
-import {DetailHero, Section, SectionHead, FeatureList, FeatureItem, PairRow, PairCard, CtaBanner, AppMock, RotatingCards, WidgetShelf, Showcase, AppCrossLinks} from '@conduction/docusaurus-preset/components';
+import {DetailHero, Section, SectionHead, FeatureList, FeatureItem, PairRow, PairCard, CtaBanner, AppMock, RotatingCards, WidgetShelf, Showcase, AgentTrace, McpToolShelf, AppCrossLinks} from '@conduction/docusaurus-preset/components';
+,
+ name: 'List open action items',
+ id: 'decidesk.listOpenActionItems',
+ description: 'Incomplete action items assigned to the caller, or all visible items. Scope mine / all, capped at 50.',
+ status: 'stable',
+ },
+ {
+ icon: ,
+ name: 'List recent meetings',
+ id: 'decidesk.listRecentMeetings',
+ description: "Recent meetings for the caller, newest first. Optional status filter: scheduled, in-progress, or closed.",
+ status: 'stable',
+ },
+ {
+ icon: ,
+ name: 'Get meeting details',
+ id: 'decidesk.getMeetingDetails',
+ description: 'Full dossier for one meeting: agenda, participants, action items, decisions, attached files.',
+ status: 'stable',
+ },
+ {
+ icon: ,
+ name: 'Start meeting',
+ id: 'decidesk.startMeeting',
+ description: 'Move a scheduled meeting into in-progress. Requires chair or admin role; logged in the audit trail.',
+ status: 'stable',
+ },
+ {
+ icon: ,
+ name: 'Add action item',
+ id: 'decidesk.addActionItem',
+ description: 'Create a new action item under a meeting, assigned to a participant, with optional due date.',
+ status: 'stable',
+ },
+ ]}
+ showIds={false}
+ trace={}
+/>
+
A self-hosted password and secrets vault for Nextcloud. Per-user vaults, team-shared secrets, an audit trail. In active development. Public beta later in 2026.>}
+ secondaryCta={{label: 'Follow progress', href: 'https://github.com/ConductionNL/doriath'}}
+ tertiaryCta={{label: 'View on GitHub', href: 'https://github.com/ConductionNL/doriath'}}
+ iconColor="var(--c-blue-cobalt)"
+ icon={}
+/>
+
+
+
+
+
+
+
+
+
+Doriath is in active development. Reach out and we'll keep you in the loop on the first public beta.>}
+ primaryCta={{label: 'Talk to a partner', href: '/partners'}}
+ secondaryCta={{label: 'View on GitHub', href: 'https://github.com/ConductionNL/doriath'}}
+/>
diff --git a/src/pages/apps/openbuilt.mdx b/src/pages/apps/openbuilt.mdx
new file mode 100644
index 000000000..6d932570c
--- /dev/null
+++ b/src/pages/apps/openbuilt.mdx
@@ -0,0 +1,173 @@
+---
+title: OpenBuilt, Conduction
+description: Citizen-developer app builder for Nextcloud. Compose apps from registers, connectors, workflows, no PHP.
+hide_table_of_contents: true
+---
+
+import {DetailHero, Section, SectionHead, FeatureList, FeatureItem, PairRow, PairCard, CtaBanner, AgentTrace, McpToolShelf, AppCrossLinks} from '@conduction/docusaurus-preset/components';
+
+An app builder for citizen developers on Nextcloud. Compose your own app from OpenRegister schemas, OpenConnector APIs, Procest workflows, and DocuDesk documents. No PHP, no scaffolding.>}
+ primaryCta={{label: 'Install from app store', href: '/install'}}
+ secondaryCta={{label: 'Read the docs', href: 'https://docs.conduction.nl/openbuilt'}}
+ tertiaryCta={{label: 'View on GitHub', href: 'https://github.com/ConductionNL/openbuilt'}}
+ iconColor="var(--c-blue-cobalt)"
+ icon={}
+/>
+
+
+
+
+
+
+ }
+ title="Visuele app-designer."
+ >
+ Kies een register, voeg pagina's toe, koppel formulieren aan schema's. Een manifest beschrijft de app; OpenBuilt rendert hem.
+
+ }
+ title="Workflows declaratief, geen service-code."
+ >
+ State machines, aggregaties, notificaties en berekeningen leef in de schema-metadata. Geen PHP-service per app, conform ADR-031.
+
+ }
+ title="Virtuele apps eerst, echte app als optie."
+ >
+ Virtuele apps draaien in de OpenBuilt-shell. Klaar voor productie? Exporteer naar een echte Nextcloud-app met eigen ID en namespace.
+
+
+
+
+
+
+
+,
+ name: 'Virtuele apps opsommen',
+ id: 'openbuilt.listApps',
+ description: 'Toon de virtuele apps in je organisatie met hun status en versie.',
+ status: 'stable',
+ },
+ {
+ icon: ,
+ name: 'App-manifest ophalen',
+ id: 'openbuilt.getAppManifest',
+ description: 'Haal het runtime-manifest van één gepubliceerde virtuele app op via de slug.',
+ status: 'stable',
+ },
+ {
+ icon: ,
+ name: 'Nieuwe app aanmaken',
+ id: 'openbuilt.createApp',
+ description: 'Maak een nieuwe virtuele app met een eerste draft-versie. Kies het versie-pad: single, dev-prod of dev-staging-prod.',
+ status: 'stable',
+ },
+ {
+ icon: ,
+ name: 'Versie promoveren',
+ id: 'openbuilt.promoteVersion',
+ description: 'Promoveer een virtuele app van bijvoorbeeld development naar productie. Default-strategie laat de doelversie leeg.',
+ status: 'stable',
+ },
+ {
+ icon: ,
+ name: 'Schema toevoegen of bijwerken',
+ id: 'openbuilt.upsertSchema',
+ description: 'Voeg een JSON Schema toe of werk het bij. De slug krijgt automatisch app- en versie-prefix.',
+ status: 'stable',
+ },
+ {
+ icon: ,
+ name: 'Pagina toevoegen of bijwerken',
+ id: 'openbuilt.upsertPage',
+ description: 'Maak of werk een pagina bij in het draft-manifest. Type is dashboard, index, detail of form.',
+ status: 'stable',
+ },
+ {
+ icon: ,
+ name: 'Widget plaatsen',
+ id: 'openbuilt.addWidget',
+ description: 'Voeg een widget toe aan een pagina: stat-counter, grafiek, lijst of een ander type uit de catalogus.',
+ status: 'stable',
+ },
+ {
+ icon: ,
+ name: 'Menu-item toevoegen of bijwerken',
+ id: 'openbuilt.upsertMenuItem',
+ description: 'Maak of werk een top-level menu-item bij in het draft-manifest. Route, volgorde en icoon stel je per item in.',
+ status: 'stable',
+ },
+ ]}
+ showIds={false}
+ trace={}
+/>
+
+
+
+
+ }
+ name="OpenRegister"
+ why="Elk virtueel app-manifest en alle objecten zijn OpenRegister-rijen."
+ />
+ }
+ name="OpenConnector"
+ why="Externe API's en bestand-bronnen koppelen zonder code."
+ />
+ }
+ name="Procest"
+ why="Workflows en goedkeurings-stappen voor virtual-app processen."
+ />
+
+
+
+OpenBuilt geeft niet-technische gebruikers de bouwblokken om eigen apps samen te stellen. Schema's, koppelvlakken, workflows, allemaal in handen van wie het probleem kent.>}
+ primaryCta={{label: 'Install from app store', href: '/install'}}
+ secondaryCta={{label: 'Talk to a partner', href: '/partners'}}
+/>
diff --git a/src/pages/apps/openregister.mdx b/src/pages/apps/openregister.mdx
index c8b4ecfa2..3685bf35a 100644
--- a/src/pages/apps/openregister.mdx
+++ b/src/pages/apps/openregister.mdx
@@ -4,7 +4,7 @@ description: Schema-driven object store with audit trail. The data foundation un
hide_table_of_contents: true
---
-import {DetailHero, Section, SectionHead, FeatureList, FeatureItem, FeatureGrid, PairRow, PairCard, CtaBanner, AppMock, RotatingCards, WidgetShelf, Showcase, AgentTrace, AppCrossLinks} from '@conduction/docusaurus-preset/components';
+import {DetailHero, Section, SectionHead, FeatureList, FeatureItem, FeatureGrid, PairRow, PairCard, CtaBanner, AppMock, RotatingCards, WidgetShelf, Showcase, AgentTrace, McpToolShelf, AppCrossLinks} from '@conduction/docusaurus-preset/components';
+
,
},
+ {
+ id: 'xwiki',
+ title: 'XWiki',
+ summary: 'Embed a live register query into any XWiki page through OpenConnector. Procedure docs stay current automatically. The article and the data live one click apart.',
+ cta: {label: 'See the XWiki integration', href: 'https://docs.conduction.nl/openregister/xwiki'},
+ panel: ,
+ },
{
id: 'llms',
title: 'LLMs (Claude, Mistral, Ollama)',
- summary: 'Plain-language search across every register. The LLM rewrites your question into a structured query, OpenRegister returns typed rows with citation-stable IDs. Every call audits.',
+ summary: 'Plain-language search across every register. The LLM rewrites your question into a structured query, OpenRegister returns typed rows with citation-stable IDs. Claude and Mistral run as cloud adapters through OpenConnector; Ollama runs as a sidecar on your own hardware. Every call audits.',
cta: {label: 'See the semantic-search adapter', href: 'https://docs.conduction.nl/openregister/llm-search'},
panel: ,
},
+ {
+ id: 'presidio',
+ title: 'Presidio (PII redaction)',
+ summary: 'Microsoft Presidio detects personal data in any register row or attached document. DocuDesk runs it before publication; OpenConnector runs it before forwarding to a cloud LLM. The register keeps the source, the outside world only sees the redacted version.',
+ cta: {label: 'See the redaction pipeline', href: 'https://docs.conduction.nl/openregister/presidio'},
+ panel: ,
+ },
+ ]}
+/>
+
+
+
+
+
+
+
+ All 24 leaves use the same pluggable integration registry. Add your own with the leaf-scaffold script and it appears on every register the moment you install your app.
+
+
+
+,
+ name: 'Manage registers',
+ id: 'openregister.registers',
+ description: 'List, fetch, create, update, or delete registers. Registers group schemas and the objects written against them.',
+ status: 'stable',
+ },
+ {
+ icon: ,
+ name: 'Manage schemas',
+ id: 'openregister.schemas',
+ description: 'List, fetch, create, update, or delete schemas. A schema is the JSON Schema that defines an object type inside a register.',
+ status: 'stable',
+ },
+ {
+ icon: ,
+ name: 'Manage objects',
+ id: 'openregister.objects',
+ description: 'List, fetch, create, update, or delete objects. Every write is validated against the schema and recorded in the signed audit log.',
+ status: 'stable',
+ },
]}
+ showIds={false}
+ trace={}
/>
diff --git a/src/pages/apps/openwoo.mdx b/src/pages/apps/openwoo.mdx
deleted file mode 100644
index 10bfe8a8e..000000000
--- a/src/pages/apps/openwoo.mdx
+++ /dev/null
@@ -1,252 +0,0 @@
----
-title: OpenWoo, Conduction
-description: WOO-compliant publication flow on Nextcloud. Active disclosure, queryable archive, citation-stable URLs.
-hide_table_of_contents: true
----
-
-import {DetailHero, Section, SectionHead, FeatureList, FeatureItem, PairRow, PairCard, CtaBanner, AppMock, RotatingCards, WidgetShelf, Showcase, AgentTrace, AppCrossLinks} from '@conduction/docusaurus-preset/components';
-
-Wet open overheid (Woo)-compliant publication flow on your Nextcloud. Eleven categories, audit log, citation-stable URLs, federation to data.overheid.nl. Citizens find what they need without filing a Woo request.>}
- primaryCta={{label: 'Install from app store', href: '/install'}}
- secondaryCta={{label: 'Read the docs', href: 'https://docs.conduction.nl/openwoo'}}
- tertiaryCta={{label: 'View on GitHub', href: 'https://github.com/ConductionNL/OpenWooApp'}}
- iconColor="var(--c-blue-cobalt)"
- icon={}
- illustration={}
-/>
-
-
-
-
-
-
- }
- title="Eleven Woo categories, pre-configured."
- >
- Laws and binding regulations, organisation and working methods, council documents, agreements, annual plans and reports, subsidies, complaints, investigations, decisions, datasets, meeting documents. All as typed registers, all in citizen-search.
-
- }
- title="Federation to data.overheid.nl."
- >
- Every published Woo record is automatically indexed on data.overheid.nl. DCAT-AP-NL metadata out of the box.
-
- }
- title="Citation-stable URLs."
- >
- Every Woo record gets a permanent identifier the moment it is first published. Parliamentary questions, legal dossiers, and journalistic articles keep working after a reorganisation or platform migration.
-
- }
- title="Audit trail per publication."
- >
- Every disclosure, edit, withdrawal, and access leaves a tamper-evident trail. The evidence pack you need for an audit, complaint, or appeal is ready.
-
- }
- title="Anonymisation upfront via DocuDesk."
- >
- DocuDesk strips PII from inbound documents before they are published. Microsoft Presidio under the hood, tuned for Dutch personal terminology.
-
-
-
-
-
-
-
-,
- },
- {
- tone: 'lavender',
- eyebrow: 'Citation-stable',
- title: 'URLs that survive a reorganisation.',
- summary: 'Every Woo record gets a permanent identifier the moment it is first published. Parliamentary questions, legal dossiers, journalistic articles keep working after platform migrations.',
- cta: {label: 'See OpenRegister', href: '/apps/openregister'},
- panel: ,
- },
- {
- tone: 'workspace',
- eyebrow: 'Federated',
- title: 'Indexed on data.overheid.nl automatically.',
- summary: 'Every published record becomes a DCAT-AP-NL entry on data.overheid.nl. Citizens find it without filing a Woo request, journalists query it without scraping.',
- cta: {label: 'See OpenCatalogi', href: '/apps/opencatalogi'},
- panel: ,
- },
- ]}
-/>
-
-
- {[
- {tone: 'var(--c-orange-knvb)', l1: '70%', l2: '50%', stage: 'redacting'},
- {tone: 'var(--c-lavender-300)', l1: '60%', l2: '45%', stage: 'review'},
- {tone: 'var(--c-red-vermillion)', l1: '65%', l2: '40%', stage: 'overdue'},
- {tone: 'var(--c-mint-500)', l1: '75%', l2: '50%', stage: 'ready'},
- {tone: 'var(--c-cobalt-300)', l1: '55%', l2: '35%', stage: 'intake'},
- ].map((row, i) => (
-
-
-
-
-
-
-
{row.stage}
-
- ))}
-
- ),
- },
- {
- title: 'Deadlines this week',
- desc: 'Statutory four-week response window. Pre-warning before the day, escalation after.',
- panel: (
-
- ),
- },
- {
- title: 'Recently published',
- desc: 'Last disclosures pushed live, with category and citation ID. One click back to the published record.',
- panel: (
-
- ),
- },
- ]}
-/>
-
-,
- },
- {
- id: 'docudesk-redact',
- title: 'DocuDesk redaction',
- summary: 'Inbound documents pass through Presidio plus an LLM second pass before publication. PII gets stripped, the cleaned version goes to the public register, the original stays sealed in archive with a tamper-evident link.',
- cta: {label: 'See the privacy pipeline', href: 'https://docs.conduction.nl/openwoo/redaction'},
- panel: ,
- },
- {
- id: 'data-overheid',
- title: 'Federation to data.overheid.nl',
- summary: 'Every published record becomes a DCAT-AP-NL entry on data.overheid.nl. OpenConnector handles the OAI-PMH push; citizens find the disclosure through the national portal without filing a separate Woo request.',
- cta: {label: 'See the federation guide', href: 'https://docs.conduction.nl/openwoo/federation'},
- panel: ,
- },
- ]}
-/>
-
-
-
-
- }
- name="OpenRegister"
- why="Each Woo category becomes a typed register. Audit log, citation-stable IDs, retention."
- />
- }
- name="OpenCatalogi"
- why="The searchable publication layer. Citizens land at jouwgemeente.nl/woo."
- />
- }
- name="DocuDesk"
- why="Anonymises documents before publication. Generates decisions per NLDS templates."
- />
-
-
-
-Drop three apps into your Nextcloud, configure the eleven categories, and your publication flow runs the same afternoon.>}
- primaryCta={{label: 'Install from app store', href: '/install'}}
- secondaryCta={{label: 'Talk to a partner', href: '/partners'}}
-/>
diff --git a/src/pages/apps/scholiq.mdx b/src/pages/apps/scholiq.mdx
new file mode 100644
index 000000000..38f1acd5e
--- /dev/null
+++ b/src/pages/apps/scholiq.mdx
@@ -0,0 +1,101 @@
+---
+title: Scholiq, Conduction
+description: Learning record + LMS on Nextcloud. Cohorts, courses, attendance, grading, certificates, compliance training.
+hide_table_of_contents: true
+---
+
+import {DetailHero, Section, SectionHead, FeatureList, FeatureItem, PairRow, PairCard, CtaBanner, AppCrossLinks} from '@conduction/docusaurus-preset/components';
+
+Leerlingvolgsysteem en LMS op Nextcloud. Cursussen, inschrijvingen, aanwezigheid, nakijken, certificaten. Voor scholen, opleiders en compliance-teams die hun leerproces niet bij een externe SaaS willen parkeren.>}
+ primaryCta={{label: 'Install from app store', href: '/install'}}
+ secondaryCta={{label: 'Read the docs', href: 'https://scholiq.conduction.nl/'}}
+ tertiaryCta={{label: 'View on GitHub', href: 'https://github.com/ConductionNL/scholiq'}}
+ iconColor="var(--c-blue-cobalt)"
+ icon={}
+/>
+
+
+
+
+
+
+ }
+ title="Cursussen en inschrijvingen."
+ >
+ Bouw een cursus uit modules, koppel lesmateriaal en data, schrijf deelnemers of hele cohorten in. Voor doorlopend onderwijs én voor één-daagse trainingen.
+
+ }
+ title="Opdrachten en nakijken."
+ >
+ Stel een opdracht uit, deelnemers leveren in, jij geeft feedback en een cijfer. De volledige geschiedenis blijft per leerling beschikbaar in zijn dossier.
+
+ }
+ title="Aanwezigheid en voortgang."
+ >
+ Wie was er, wie heeft de module afgerond, wie staat waar in zijn leerplan. Zonder losse Excel-lijsten, zonder schaduw-administratie.
+
+ }
+ title="Certificaten en compliance-training."
+ >
+ Reik certificaten uit met geldigheid en hercertificering. Voor BHV, AVG, integriteits-trainingen of de jaarlijkse cybersecurity-cursus: het systeem herinnert je vanzelf wie weer aan de beurt is.
+
+
+
+
+
+
+
+
+
+
+ }
+ name="OpenRegister"
+ why="Deelnemers, cursussen, inschrijvingen, cijfers en certificaten als typed registers."
+ />
+ }
+ name="DocuDesk"
+ why="Genereert certificaten, cursusverslagen en deelnemerslijsten uit sjablonen."
+ />
+ }
+ name="MyDash"
+ why="Mijn cursussen, na te kijken werk, en hercertificeringen op het dashboard."
+ />
+
+
+
+Scholiq houdt het leerproces — van cursusinschrijving tot certificering — op je eigen Nextcloud. Open-source, op je eigen data, klaar voor inspectie en compliance.>}
+ primaryCta={{label: 'Install from app store', href: '/install'}}
+ secondaryCta={{label: 'Talk to a partner', href: '/partners'}}
+/>
diff --git a/src/pages/apps/shillinq.mdx b/src/pages/apps/shillinq.mdx
new file mode 100644
index 000000000..45a64ea12
--- /dev/null
+++ b/src/pages/apps/shillinq.mdx
@@ -0,0 +1,95 @@
+---
+title: Shillinq, Conduction
+description: Business administration on Nextcloud. Invoices, contracts, procurement on shared registers.
+hide_table_of_contents: true
+---
+
+import {DetailHero, Section, SectionHead, FeatureList, FeatureItem, PairRow, PairCard, CtaBanner, AppCrossLinks} from '@conduction/docusaurus-preset/components';
+
+Business administration on Nextcloud. Invoices, contracts, procurement on shared registers. Built for the MKB that already runs Nextcloud and wants its finance, sales, and procurement in the same workspace.>}
+ primaryCta={{label: 'Install from app store', href: '/install'}}
+ secondaryCta={{label: 'Read the docs', href: 'https://docs.conduction.nl/shillinq'}}
+ tertiaryCta={{label: 'View on GitHub', href: 'https://github.com/ConductionNL/shillinq'}}
+ iconColor="var(--c-blue-cobalt)"
+ icon={}
+/>
+
+
+
+
+
+
+ }
+ title="Facturatie en debiteuren."
+ >
+ Genereer facturen uit sjablonen. Stuur ze direct uit Nextcloud Mail. Volg openstaande bedragen in MyDash.
+
+ }
+ title="Contracten met audit trail."
+ >
+ Versioned contracten in OpenRegister. Zie wie wat heeft getekend, wanneer een verlenging vervalt, welke voorwaarden gelden.
+
+ }
+ title="Inkoop met goedkeurings-flow."
+ >
+ PO-aanvragen, goedkeuring per drempelbedrag, leveranciersregister. Voor de bestelling de hand op de knip houden, achteraf de factuur erbij.
+
+
+
+
+
+
+
+
+
+
+ }
+ name="OpenRegister"
+ why="Facturen, contracten, inkooporders als typed registers."
+ />
+ }
+ name="DocuDesk"
+ why="Genereert facturen, offertes en contracten uit sjablonen."
+ />
+ }
+ name="MyDash"
+ why="Cash-flow, omzet, top-debiteuren als widgets op het dashboard."
+ />
+
+
+
+Shillinq vervangt losse boekhoudpakketten en CRM-tools door één bedrijfsadministratie op Nextcloud. Open-source, op je eigen data.>}
+ primaryCta={{label: 'Install from app store', href: '/install'}}
+ secondaryCta={{label: 'Talk to a partner', href: '/partners'}}
+/>
diff --git a/src/pages/commonground.mdx b/src/pages/commonground.mdx
index 7402bad57..7bcd64e05 100644
--- a/src/pages/commonground.mdx
+++ b/src/pages/commonground.mdx
@@ -5,6 +5,7 @@ hide_table_of_contents: true
---
import {Hero, StatsStrip, Section, SectionHead, AppsPreview, PairRow, PairCard, CtaBanner, Pill, Showcase, AppMock, CommonGroundPlus, ManagedCommonGround} from '@conduction/docusaurus-preset/components';
+import {APP_COUNT} from '@site/src/data/apps-catalog';
import {totalDownloads, formatDownloads} from '@conduction/docusaurus-preset/data';
import {totalPartners} from '@site/src/data/partners-catalog';
@@ -18,7 +19,7 @@ import {totalPartners} from '@site/src/data/partners-catalog';
/>
apps in the ecosystem, all open-source> },
+ { value: String(APP_COUNT), label: <>apps in the ecosystem, all open-source> },
{ value: '2 min', label: <>from app store install to working solution> },
{ value: '€0', label: <>for the apps, support is optional> },
{ value: totalPartners, label: <>certified and partner implementers> },
@@ -31,12 +32,12 @@ import {totalPartners} from '@site/src/data/partners-catalog';
lede={<>Start with what your WOO coordinator, GGU contact, or procurement committee is asking for today. All open-source, GDPR-compliant, free to install from the Nextcloud app store.>}
apps={[
{
- name: 'OpenWoo',
- tagline: 'WOO publication flow. Active disclosure, queryable archive, automatic redaction of personal data.',
+ name: 'DocuDesk',
+ tagline: 'Auto-classify, anonymise, sign, and template documents. Powers the WOO publication flow end to end.',
status: 'STABLE',
- version: 'v1.4 · NL',
- href: '/apps/openwoo',
- icon: ,
+ version: 'v1.x · NL · EN',
+ href: '/apps/docudesk',
+ icon: ,
},
{
name: 'OpenCatalogi',
@@ -58,6 +59,35 @@ import {totalPartners} from '@site/src/data/partners-catalog';
seeAll={{label: 'Browse all apps', href: '/apps'}}
/>
+
+ OpenWoo is geen aparte app, het is een solution die OpenRegister, OpenCatalogi en DocuDesk samenbrengt voor de Wet open overheid. Eén Woo-categorieenboom op typed registers, automatische redactie van persoonsgegevens, federatie naar jouwgemeente.nl/openwoo. Geen procurement-traject, geen consultancy-uren, één installatie.>}
+ />
+
+ }
+ name="OpenRegister"
+ why="Woo-categorieen, publicaties, retentie-termijnen als typed registers."
+ />
+ }
+ name="OpenCatalogi"
+ why="Publiceert het archief naar jouwgemeente.nl/openwoo met federation."
+ />
+ }
+ name="DocuDesk"
+ why="Auto-redactie van persoonsgegevens voor publicatie. Audit trail per document."
+ />
+
+
+
Twelve open-source apps · one Nextcloud>}
+ eyebrow={<>{APP_COUNT} open-source apps · one Nextcloud>}
title={<>Install your stack. In two minutes.>}
- lede={<>Twelve open-source apps that plug into Nextcloud. Catalogs, registers, integrations, document handling, pick what you need from the app store and you're working today. No methodology workshop. No lock-in.>}
+ lede={<>{APP_COUNT} open-source apps that plug into Nextcloud. Catalogs, registers, integrations, document handling, pick what you need from the app store and you're working today. No methodology workshop. No lock-in.>}
primaryCta={{label: <>Install from Nextcloud app store>, href: '/apps'}}
secondaryCta={{label: 'Run a local demo', href: '/demo'}}
tertiaryCta={{label: 'View on GitHub', href: 'https://github.com/ConductionNL'}}
@@ -25,7 +26,7 @@ import {totalPartners} from '@site/src/data/partners-catalog';
apps in the ecosystem, all open-source> },
+ { value: String(APP_COUNT), label: <>apps in the ecosystem, all open-source> },
{ value: '2 min', label: <>from app store install to working solution> },
{ value: '€0', label: <>for the apps, support is optional> },
{ value: totalPartners, label: <>certified and partner implementers> },
@@ -34,7 +35,7 @@ import {totalPartners} from '@site/src/data/partners-catalog';
Five components. Twenty-four apps. One Nextcloud workspace.>}
+ title={<>Five components. {ECOSYSTEM_COUNT} apps. One Nextcloud workspace.>}
lede={<>Every ConNext app plugs into one of five categories around the Nextcloud workspace, files, mail, calendar, talk, office. All of them run on the platform your team already trusts. Hover any app to see what it does.>}
>
},
]},
{position: 'top-left', family: 'core', label: 'Technical\nCore', items: [
- {name: 'OpenRegister', meta: 'Data', desc: 'Schema-driven object store with audit trail. The data foundation underneath every workspace app.', icon: },
- {name: 'OpenCatalogi', meta: 'Catalog', desc: 'Federated data catalogue. Datasets, APIs, software inventory.', icon: },
- {name: 'OpenConnector', meta: 'Integrate', desc: 'API gateway, REST, SOAP, GraphQL, webhooks.', icon: },
- {name: 'DocuDesk', meta: 'Docs', desc: 'Generate, anonymise, sign, and template documents.', icon: },
- {name: 'NLDesign Theme', meta: 'Theme', desc: 'NL Design System tokens applied across every Conduction app.', icon: },
- {name: 'MyDash', meta: 'Dashboard', desc: 'Dashboard builder with role-based access.', icon: },
+ {name: 'OpenRegister', meta: 'Data', href: '/apps/openregister', desc: 'Schema-driven object store with audit trail. The data foundation underneath every workspace app.', icon: },
+ {name: 'OpenCatalogi', meta: 'Catalog', href: '/apps/opencatalogi', desc: 'Federated data catalogue. Datasets, APIs, software inventory.', icon: },
+ {name: 'OpenConnector', meta: 'Integrate', href: '/apps/openconnector', desc: 'API gateway, REST, SOAP, GraphQL, webhooks.', icon: },
+ {name: 'DocuDesk', meta: 'Docs', href: '/apps/docudesk', desc: 'Generate, anonymise, sign, and template documents.', icon: },
+ {name: 'NLDesign Theme', meta: 'Theme', href: '/apps/nldesign', desc: 'NL Design System tokens applied across every Conduction app.', icon: },
+ {name: 'MyDash', meta: 'Dashboard', href: '/apps/mydash', desc: 'Dashboard builder with role-based access.', icon: },
+ {name: 'App Versions', meta: 'Versions', href: '/apps/app-versions', desc: 'Pin and roll back any Nextcloud app version. Safety net for fleet upgrades.', icon: },
]},
{position: 'top-right', family: 'solutions', label: 'Solutions', items: [
- {name: 'OpenWoo', meta: 'WOO', desc: 'WOO-compliant publication flow. Active disclosure, queryable archive.', icon: },
- {name: 'ZaakAfhandelApp', meta: 'Zaken', desc: 'Citizen-facing case-status portal.', icon: },
- {name: 'Anonimiseren', meta: 'Privacy', desc: 'Privacy-by-design pipeline. Auto-redact PII before publication.', icon: },
+ {name: 'OpenWoo', meta: 'WOO', href: '/commonground#openwoo', desc: 'WOO-compliant publication flow. Active disclosure, queryable archive. Solution built on OpenRegister + OpenCatalogi + DocuDesk.', icon: },
+ {name: 'ZaakAfhandelApp', meta: 'Zaken', href: '/apps/zaakafhandelapp', desc: 'Citizen-facing case-status portal.', icon: },
+ {name: 'Anonimiseren', meta: 'Privacy', href: '/apps/docudesk', desc: 'Privacy-by-design pipeline. Auto-redact PII before publication.', icon: },
+ {name: 'Doriath', meta: 'Vault', href: '/apps/doriath', desc: 'Self-hosted password and secrets vault. Per-user, per-team, audited.', icon: },
]},
{position: 'bottom-left', family: 'workspace', label: 'Workspace\nApps', items: [
- {name: 'PipelinQ', meta: 'CRM', desc: 'CRM with quotes, contacts, and deal-flow.', icon: },
- {name: 'Procest', meta: 'Cases', desc: 'Case-management for VTH and citizen processes.', icon: },
- {name: 'DeciDesk', meta: 'Boards', desc: 'Decision-support and board management.', icon: },
- {name: 'ShillinQ', meta: 'ERP', desc: 'Bookkeeping, invoicing, expense tracking.', icon: },
- {name: 'LarpingApp', meta: 'Larp', desc: 'Worldbuilding and campaign-management.', icon: },
- {name: 'SoftwareCatalog', meta: 'Assets', desc: 'IT-asset management, software inventory, licenses, contracts.', icon: },
+ {name: 'PipelinQ', meta: 'CRM', href: '/apps/pipelinq', desc: 'CRM with quotes, contacts, and deal-flow.', icon: },
+ {name: 'Procest', meta: 'Cases', href: '/apps/procest', desc: 'Case-management for VTH and citizen processes.', icon: },
+ {name: 'DeciDesk', meta: 'Boards', href: '/apps/decidesk', desc: 'Decision-support and board management.', icon: },
+ {name: 'ShillinQ', meta: 'ERP', href: '/apps/shillinq', desc: 'Bookkeeping, invoicing, expense tracking.', icon: },
+ {name: 'LarpingApp', meta: 'Larp', href: '/apps/larpingapp', desc: 'Worldbuilding and campaign-management.', icon: },
+ {name: 'SoftwareCatalog', meta: 'Assets', href: '/apps/softwarecatalog', desc: 'IT-asset management, software inventory, licenses, contracts.', icon: },
+ {name: 'Scholiq', meta: 'LMS', href: '/apps/scholiq', desc: 'Learning record + LMS. Courses, enrolment, certificates, compliance-training.', icon: },
]},
{position: 'bottom-right', family: 'integrate', label: 'Integrated\nApps', columns: 2, items: [
- {name: 'OpenTalk', meta: 'Video', brandColor: '#00485C', desc: 'Open-source video conferencing.', icon: },
{name: 'Matrix', meta: 'Chat', brandColor: '#000000', desc: 'Federated chat protocol. End-to-end encrypted.', icon: },
{name: 'n8n', meta: 'Flows', brandColor: '#EA4B71', desc: 'Workflow automation, no-code flows.', icon: },
{name: 'Windmill', meta: 'Flow', brandColor: '#DC2A1F', desc: 'Developer-focused workflow engine. Python, TypeScript, Go, Bash.', icon: },
@@ -87,10 +90,8 @@ import {totalPartners} from '@site/src/data/partners-catalog';
{name: 'Presidio', meta: 'PII', brandColor: '#0078D4', desc: 'Microsoft PII detection. Anonymise text before publication, GDPR-ready.', icon: },
{name: 'Keycloak', meta: 'SSO', brandColor: '#DB3327', desc: 'Open-source identity and access management. SSO, OIDC, SAML, social login.', icon: },
]},
- {position: 'bottom-center', family: 'builder', label: 'App\nBuilder', badge: 'COMING SOON', items: [
- {name: 'Build your own apps', desc: 'Compose Nextcloud apps from the Technical Core.', icon: },
- {name: 'Customise ConNext apps', desc: 'Fork-free customisation of any ConNext app.', icon: },
- {name: 'No code, just configure', desc: 'Configuration-only changes survive upgrades.', icon: },
+ {position: 'bottom-center', family: 'builder', label: 'App\nBuilder', items: [
+ {name: 'OpenBuilt', meta: 'Builder', href: '/apps/openbuilt', desc: 'Compose Nextcloud apps from the Technical Core. Fork-free customisation. Configuration over code, so changes survive upgrades.', icon: },
]},
]}
flows={[
@@ -182,7 +183,7 @@ import {totalPartners} from '@site/src/data/partners-catalog';
Most Conduction integrations are with the standard Nextcloud apps already on the home screen, Mail, Files, Calendar, Contacts, Notes, Deck. The set below is different. These are external open-source projects we did not build. We host them next to Nextcloud as ExApps (separate processes the workspace runs and exposes through its UI), or call them through OpenConnector adapters. Reusing them is cheaper than rewriting them, and it keeps the credit where it belongs.>}
@@ -192,28 +193,27 @@ import {totalPartners} from '@site/src/data/partners-catalog';
Why through the app store?
- Each ExApp is packaged for the Nextcloud app store, so installing one feels the same as installing any other Nextcloud app. We ship the option because sovereignty without choice is just a slogan. Like Nextcloud Talk with its tight workspace fit? Use it. Prefer OpenTalk on the Matrix protocol? Use that instead. Same login, same workspace, your call on the engine.
+ Each ExApp is packaged for the Nextcloud app store, so installing one feels the same as installing any other Nextcloud app. We ship the option because sovereignty without choice is just a slogan. Like Nextcloud Talk with its tight workspace fit? Use it. Prefer a federated chat like Matrix? Use that instead. Same login, same workspace, your call on the engine.
},
- {name: 'Matrix', meta: 'ExApp · Chat', brandColor: '#000000', desc: 'Federated chat protocol. End-to-end encrypted, runs on your own server.', href: 'https://matrix.org/', icon: },
- {name: 'Mattermost', meta: 'ExApp · Chat', brandColor: '#0058CC', desc: 'Self-hosted Slack alternative. Threads, integrations, and on-prem data.', href: 'https://mattermost.com/', icon: },
- {name: 'OpenExchange',meta: 'ExApp · Mail', brandColor: '#005A9B', desc: 'Open-source mail and groupware suite. Drop-in alternative to Microsoft Exchange, with App Suite for the inbox.', href: 'https://www.open-xchange.com/', icon: },
- {name: 'Windmill', meta: 'ExApp · Workflow', brandColor: '#DC2A1F', desc: 'Developer-focused workflow engine. Python, TypeScript, Go, Bash. Triggered by Conduction events.', href: 'https://windmill.dev/', icon: },
- {name: 'n8n', meta: 'ExApp · Workflow', brandColor: '#EA4B71', desc: 'Low-code automation flows. Drag-drop builder, hundreds of node types, fits next to OpenConnector.', href: 'https://n8n.io/', icon: },
- {name: 'OpenProject', meta: 'ExApp · PM', brandColor: '#0770B8', desc: 'Project management. Gantt, agile boards, work packages mapped to Conduction cases.', href: 'https://www.openproject.org/', icon: },
- {name: 'XWiki', meta: 'ExApp · Wiki', brandColor: '#1E4FB6', desc: 'Structured wiki and knowledge base. Pages, macros, query language. Embed register data inline.', href: 'https://www.xwiki.org/', icon: },
- {name: 'GitLab', meta: 'ExApp · Code', brandColor: '#FC6D26', desc: 'Git hosting, CI/CD, issue tracking. The other half of every developer team.', href: 'https://gitlab.com/', icon: },
- {name: 'Keycloak', meta: 'ExApp · SSO', brandColor: '#DB3327', desc: 'Open-source identity. SSO, OIDC, SAML, social login. Stand-in for the gemeentelijke IdP.', href: 'https://www.keycloak.org/', icon: },
- {name: 'Claude', meta: 'Adapter · LLM', brandColor: '#D97757', desc: 'Anthropic frontier LLM. Long context, strong agents, called via the OpenConnector adapter.', href: 'https://www.anthropic.com/', icon: },
- {name: 'OpenAI', meta: 'Adapter · LLM', brandColor: '#10A37F', desc: 'GPT family. Drop-in adapter through OpenConnector when policy allows external LLMs.', href: 'https://openai.com/', icon: },
- {name: 'Mistral', meta: 'Adapter · LLM', brandColor: '#FA500F', desc: 'EU-hosted open-weight LLM. Strong reasoning, GDPR-friendly, the default for compliance-heavy installs.', href: 'https://mistral.ai/', icon: },
- {name: 'Ollama', meta: 'Adapter · LLM', brandColor: '#000000', desc: 'Self-hosted LLM runtime. Run Mistral, Llama, and more on your own GPU. No data leaves the server.', href: 'https://ollama.com/', icon: },
- {name: 'Presidio', meta: 'Adapter · PII', brandColor: '#0078D4', desc: 'Microsoft\'s PII detection library. Anonymises text before publication. Powers DocuDesk\'s redact_pii tool.', href: 'https://microsoft.github.io/presidio/', icon: },
+ {name: 'Matrix', meta: 'ExApp · Chat', brandColor: '#000000', desc: 'Federated chat protocol. End-to-end encrypted, runs on your own server.', href: 'https://apps.nextcloud.com/apps/integration_matrix', icon: },
+ {name: 'Mattermost', meta: 'ExApp · Chat', brandColor: '#0058CC', desc: 'Self-hosted Slack alternative. Threads, integrations, and on-prem data.', href: 'https://apps.nextcloud.com/apps/integration_mattermost', icon: },
+ {name: 'OpenExchange',meta: 'ExApp · Mail', brandColor: '#005A9B', desc: 'Open-source mail and groupware suite. Drop-in alternative to Microsoft Exchange, with App Suite for the inbox.', href: 'https://nextcloud.com/partners/', icon: },
+ {name: 'Windmill', meta: 'ExApp · Workflow', brandColor: '#DC2A1F', desc: 'Developer-focused workflow engine. Python, TypeScript, Go, Bash. Triggered by Conduction events.', href: 'https://apps.nextcloud.com/apps/integration_windmill', icon: },
+ {name: 'n8n', meta: 'ExApp · Workflow', brandColor: '#EA4B71', desc: 'Low-code automation flows. Drag-drop builder, hundreds of node types, fits next to OpenConnector.', href: '/sidecars/n8n', icon: },
+ {name: 'OpenProject', meta: 'ExApp · PM', brandColor: '#0770B8', desc: 'Project management. Gantt, agile boards, work packages mapped to Conduction cases.', href: 'https://apps.nextcloud.com/apps/integration_openproject', icon: },
+ {name: 'XWiki', meta: 'ExApp · Wiki', brandColor: '#1E4FB6', desc: 'Structured wiki and knowledge base. Pages, macros, query language. Embed register data inline.', href: '/apps/openregister#integrations', icon: },
+ {name: 'GitLab', meta: 'ExApp · Code', brandColor: '#FC6D26', desc: 'Git hosting, CI/CD, issue tracking. The other half of every developer team.', href: 'https://apps.nextcloud.com/apps/integration_gitlab', icon: },
+ {name: 'Keycloak', meta: 'ExApp · SSO', brandColor: '#DB3327', desc: 'Open-source identity. SSO, OIDC, SAML, social login. Stand-in for the gemeentelijke IdP.', href: '/sidecars/keycloak', icon: },
+ {name: 'Claude', meta: 'Adapter · LLM', brandColor: '#D97757', desc: 'Anthropic frontier LLM. Long context, strong agents, called via the OpenConnector adapter.', href: '/apps/openregister#integrations', icon: },
+ {name: 'OpenAI', meta: 'Adapter · LLM', brandColor: '#10A37F', desc: 'GPT family. Drop-in adapter through OpenConnector when policy allows external LLMs.', href: 'https://apps.nextcloud.com/apps/integration_openai', icon: },
+ {name: 'Mistral', meta: 'Adapter · LLM', brandColor: '#FA500F', desc: 'EU-hosted open-weight LLM. Strong reasoning, GDPR-friendly, the default for compliance-heavy installs.', href: '/apps/openregister#integrations', icon: },
+ {name: 'Ollama', meta: 'Adapter · LLM', brandColor: '#000000', desc: 'Self-hosted LLM runtime. Run Mistral, Llama, and more on your own GPU. No data leaves the server.', href: '/sidecars/ollama', icon: },
+ {name: 'Presidio', meta: 'Adapter · PII', brandColor: '#0078D4', desc: 'Microsoft\'s PII detection library. Anonymises text before publication. Powers DocuDesk\'s redact_pii tool.', href: '/apps/openregister#integrations', icon: },
]}
/>
diff --git a/src/pages/demo.mdx b/src/pages/demo.mdx
index 56e80602a..68de1b7d7 100644
--- a/src/pages/demo.mdx
+++ b/src/pages/demo.mdx
@@ -5,6 +5,7 @@ hide_table_of_contents: true
---
import {Section, SectionHead, HowSteps, HowStep, FeatureList, FeatureItem, PairRow, PairCard, CtaBanner, Button, ComposeBlock} from '@conduction/docusaurus-preset/components';
+import {APP_COUNT} from '@site/src/data/apps-catalog';
href="/apps/openconnector"
icon={}
name="OpenConnector"
- why="Trekt voorbeelddata binnen via REST/SOAP/file drops. Alle 12 apps profiteren."
+ why={<>Trekt voorbeelddata binnen via REST/SOAP/file drops. Alle {APP_COUNT} apps profiteren.>}
/>
diff --git a/src/pages/index.mdx b/src/pages/index.mdx
index c35934655..96cb3d9d0 100644
--- a/src/pages/index.mdx
+++ b/src/pages/index.mdx
@@ -7,6 +7,7 @@ hide_table_of_contents: true
import {Hero, StatsStrip, AppsPreview, CtaBanner, PairRow, PairCard, Section, SectionHead, Clients, DEFAULT_CLIENTS, PartnerDirectory, CommonGroundPlus, HexRain} from '@conduction/docusaurus-preset/components';
import {totalDownloads, formatDownloads} from '@conduction/docusaurus-preset/data';
import {PARTNERS, BECOME_PARTNER, totalPartners} from '@site/src/data/partners-catalog';
+import {APP_COUNT} from '@site/src/data/apps-catalog';
{/* Conduction main landing. Public Tech framing: tech to serve people.
Three doors out: install an app yourself, get it as managed SaaS, or
@@ -17,7 +18,7 @@ import {PARTNERS, BECOME_PARTNER, totalPartners} from '@site/src/data/partners-c
Public Tech · open-source apps for Nextcloud>}
- title={<>Tech to serve people. One workspace. Twelve apps.>}
+ title={<>Tech to serve people. One workspace. {APP_COUNT} apps.>}
lede={<>We are Conduction. We build open-source apps that plug into Nextcloud. Catalogs, registers, document handling, integrations. Install yourself, host with us as SaaS, or bring in a partner. Same code, same licence, no procurement track required.>}
primaryCta={{label: <>Install from Nextcloud app store>, href: '/apps'}}
secondaryCta={{label: 'Run a local demo', href: '/demo'}}
@@ -27,7 +28,7 @@ import {PARTNERS, BECOME_PARTNER, totalPartners} from '@site/src/data/partners-c
apps in the ecosystem, all open-source> },
+ { value: String(APP_COUNT), label: <>apps in the ecosystem, all open-source> },
{ value: '2 min', label: <>from app store install to working solution> },
{ value: '€0', label: <>for the apps, support is optional> },
{ value: totalPartners, label: <>certified and partner implementers> },
diff --git a/src/pages/partners/acato.mdx b/src/pages/partners/acato.mdx
index 8ac7898fa..ea8fa9a19 100644
--- a/src/pages/partners/acato.mdx
+++ b/src/pages/partners/acato.mdx
@@ -1,5 +1,5 @@
---
-title: Acato, Strategic partner, Conduction
+title: Acato, Certified partner, Conduction
description: Nederlandse open-source specialist uit Almere. Bouwt en host Nextcloud-omgevingen voor gemeenten en zorginstellingen.
hide_table_of_contents: true
---
@@ -13,7 +13,7 @@ export const ACATO_SOLUTIONS = solutionsBySlugs(ACATO.solutions);
diff --git a/src/pages/partners/procolix.mdx b/src/pages/partners/procolix.mdx
index 16740f265..edb188477 100644
--- a/src/pages/partners/procolix.mdx
+++ b/src/pages/partners/procolix.mdx
@@ -1,6 +1,6 @@
---
-title: Procolix, Certified partner, Conduction
-description: Officieel Nextcloud-distributeur. Certified op DocuDesk en OpenConnector voor procesintegraties bij gemeenten en uitvoeringsorganisaties.
+title: Procolix, Host partner, Conduction
+description: Nederlandse managed-Nextcloud-hoster uit Dordrecht. Levert beheerde Nextcloud-omgevingen op eigen EU-infrastructuur, open source als uitgangspunt.
hide_table_of_contents: true
---
@@ -13,11 +13,11 @@ export const PROCOLIX_SOLUTIONS = solutionsBySlugs(PROCOLIX.solutions);
Nederlandse hoster en leverancier van proces- en zaakmanagement-software. Officieel Nextcloud-distributeur. Certified op DocuDesk en OpenConnector voor procesintegraties bij gemeenten en uitvoeringsorganisaties.>}
+ tagline={<>Nederlandse managed-Nextcloud-hoster uit Dordrecht. Levert beheerde Nextcloud-omgevingen op eigen EU-infrastructuur, open source als uitgangspunt. Conduction-apps draaien daar bovenop voor klanten die hosting + apps bij één leverancier willen.>}
primaryCta={{label: 'Talk to Procolix', href: 'mailto:hello@procolix.com'}}
secondaryCta={{label: 'Visit procolix.com', href: 'https://procolix.com'}}
tertiaryCta={{label: 'All partners', href: '/partners'}}
@@ -101,18 +101,18 @@ export const PROCOLIX_SOLUTIONS = solutionsBySlugs(PROCOLIX.solutions);
diff --git a/src/pages/partners/yard.mdx b/src/pages/partners/yard.mdx
index 61fc9261d..5156f55a0 100644
--- a/src/pages/partners/yard.mdx
+++ b/src/pages/partners/yard.mdx
@@ -1,5 +1,5 @@
---
-title: YARD, Partner, Conduction
+title: YARD, Host partner, Conduction
description: Digital design- en development-bureau uit Utrecht. Strong on MyDash and OpenCatalogi for citizen-facing surfaces.
hide_table_of_contents: true
---
@@ -13,7 +13,7 @@ export const YARD_SOLUTIONS = solutionsBySlugs(YARD.solutions);
diff --git a/src/pages/sidecars.mdx b/src/pages/sidecars.mdx
new file mode 100644
index 000000000..bf93e7a0d
--- /dev/null
+++ b/src/pages/sidecars.mdx
@@ -0,0 +1,80 @@
+---
+title: Sidecars, Conduction
+description: Nextcloud-installable wrappers around third-party open-source services. Same upstream, less DevOps.
+hide_table_of_contents: true
+---
+
+import React, {useState, useMemo} from 'react';
+import {Section, SectionHead, FacetedFilters, AppCard} from '@conduction/docusaurus-preset/components';
+import {getSidecars, SIDECAR_CATEGORIES} from '@site/src/data/sidecars-catalog';
+
+export const sidecars = getSidecars();
+
+export const STATUS_LABELS = {STABLE: 'Stable', BETA: 'Beta', 'COMING SOON': 'Coming soon'};
+
+export function FilterableSidecars() {
+ const [selected, setSelected] = useState({});
+
+ const facets = useMemo(() => {
+ const cats = SIDECAR_CATEGORIES.filter(c => c !== 'All');
+ return [
+ {key: 'category', label: 'Category', items: cats.map(c => ({
+ value: c, label: c,
+ count: sidecars.filter(a => Array.isArray(a.categories) && a.categories.includes(c)).length,
+ })).filter(item => item.count > 0)},
+ {key: 'status', label: 'Status', items: ['STABLE', 'BETA', 'COMING SOON'].map(s => ({
+ value: s, label: STATUS_LABELS[s],
+ count: sidecars.filter(a => a.status === s).length,
+ })).filter(item => item.count > 0)},
+ ];
+ }, []);
+
+ const filtered = useMemo(() => {
+ return sidecars.filter(a => {
+ for (const [key, values] of Object.entries(selected)) {
+ if (!values || values.length === 0) continue;
+ if (key === 'category') {
+ const cats = a.categories || [];
+ if (!values.some(v => cats.includes(v))) return false;
+ } else if (key === 'status') {
+ if (!values.includes(a.status)) return false;
+ }
+ }
+ return true;
+ });
+ }, [selected]);
+
+ const totalSelected = Object.values(selected).reduce(
+ (n, v) => n + (Array.isArray(v) ? v.length : 0), 0
+ );
+
+ return (
+
+
+
+
+
+ {filtered.length} of {sidecars.length} sidecars
+ {totalSelected > 0 && · {sidecars.length - filtered.length} hidden by filters}
+
+
+
+ {filtered.map((app, i) => )}
+
+ {filtered.length === 0 && (
+
No sidecars match this filter.
+ )}
+
+
+ );
+}
+
+
+ Third-party services, installed like an app.>}
+ lede={<>Sidecars are Nextcloud-installable wrappers around proven open-source services. n8n, Keycloak, OpenZaak, Ollama and others, packaged so you install them from the app store instead of operating a separate cluster. We do not fork the upstream; the sidecar tracks releases and handles the koppelvlak to Nextcloud.>}
+ />
+
+
+
diff --git a/src/pages/sidecars/keycloak.mdx b/src/pages/sidecars/keycloak.mdx
new file mode 100644
index 000000000..c78ce646d
--- /dev/null
+++ b/src/pages/sidecars/keycloak.mdx
@@ -0,0 +1,47 @@
+---
+title: Keycloak sidecar, Conduction
+description: Nextcloud sidecar wrapping Keycloak for self-hosted identity and SSO.
+hide_table_of_contents: true
+---
+
+import {DetailHero, Section, SectionHead, CtaBanner} from '@conduction/docusaurus-preset/components';
+
+A Nextcloud sidecar that wraps Keycloak. Run the open-source identity broker inside your workspace, installed from the app store, SSO across all your apps from day one.>}
+ primaryCta={{label: 'Install from app store', href: '/install'}}
+ secondaryCta={{label: 'Read the docs', href: 'https://docs.conduction.nl/keycloak'}}
+ tertiaryCta={{label: 'View on GitHub', href: 'https://github.com/ConductionNL/keycloak-nextcloud'}}
+ iconColor="var(--c-blue-cobalt)"
+ icon={}
+/>
+
+
+
+
+
+
+
+
+
+Install from the Nextcloud app store. Configure SSO for every Conduction app and every third-party sidecar in one place.>}
+ primaryCta={{label: 'Install from app store', href: '/install'}}
+ secondaryCta={{label: 'Talk to a partner', href: '/partners'}}
+/>
diff --git a/src/pages/sidecars/n8n.mdx b/src/pages/sidecars/n8n.mdx
new file mode 100644
index 000000000..8a1cf4d51
--- /dev/null
+++ b/src/pages/sidecars/n8n.mdx
@@ -0,0 +1,47 @@
+---
+title: n8n sidecar, Conduction
+description: Nextcloud sidecar wrapping n8n for workflow automation.
+hide_table_of_contents: true
+---
+
+import {DetailHero, Section, SectionHead, CtaBanner} from '@conduction/docusaurus-preset/components';
+
+A Nextcloud sidecar that wraps n8n. Run the popular visual workflow-automation engine inside your workspace, installed from the Nextcloud app store, no separate hosting.>}
+ primaryCta={{label: 'Install from app store', href: '/install'}}
+ secondaryCta={{label: 'Read the docs', href: 'https://docs.conduction.nl/n8n'}}
+ tertiaryCta={{label: 'View on GitHub', href: 'https://github.com/ConductionNL/n8n-nextcloud'}}
+ iconColor="var(--c-blue-cobalt)"
+ icon={}
+/>
+
+
+
+
+
+
+
+
+
+Install from the Nextcloud app store. Connect to OpenRegister rows. Trigger workflows from OpenConnector events. Surface results in MyDash.>}
+ primaryCta={{label: 'Install from app store', href: '/install'}}
+ secondaryCta={{label: 'Talk to a partner', href: '/partners'}}
+/>
diff --git a/src/pages/sidecars/ollama.mdx b/src/pages/sidecars/ollama.mdx
new file mode 100644
index 000000000..9d825fe6e
--- /dev/null
+++ b/src/pages/sidecars/ollama.mdx
@@ -0,0 +1,47 @@
+---
+title: Ollama sidecar, Conduction
+description: Nextcloud sidecar wrapping Ollama for local LLM inference.
+hide_table_of_contents: true
+---
+
+import {DetailHero, Section, SectionHead, CtaBanner} from '@conduction/docusaurus-preset/components';
+
+A Nextcloud sidecar that wraps Ollama. Run local large-language models inside your workspace, install from the app store, keep your prompts on your own hardware.>}
+ primaryCta={{label: 'Install from app store', href: '/install'}}
+ secondaryCta={{label: 'Read the docs', href: 'https://docs.conduction.nl/ollama'}}
+ tertiaryCta={{label: 'View on GitHub', href: 'https://github.com/ConductionNL/ollama-nextcloud'}}
+ iconColor="var(--c-blue-cobalt)"
+ icon={}
+/>
+
+
+
+
+
+
+
+
+
+Install from the Nextcloud app store. Wire local models into AI Companion. Drive summarisation flows from OpenConnector. Keep every prompt on your own hardware.>}
+ primaryCta={{label: 'Install from app store', href: '/install'}}
+ secondaryCta={{label: 'Talk to a partner', href: '/partners'}}
+/>
diff --git a/src/pages/sidecars/open-webui.mdx b/src/pages/sidecars/open-webui.mdx
new file mode 100644
index 000000000..358c35a5b
--- /dev/null
+++ b/src/pages/sidecars/open-webui.mdx
@@ -0,0 +1,47 @@
+---
+title: Open WebUI sidecar, Conduction
+description: Nextcloud sidecar wrapping Open WebUI for a chat interface over local LLMs.
+hide_table_of_contents: true
+---
+
+import {DetailHero, Section, SectionHead, CtaBanner} from '@conduction/docusaurus-preset/components';
+
+A Nextcloud sidecar that wraps Open WebUI. A ChatGPT-style interface for your local LLMs, installed from the app store, all running on your own hardware.>}
+ primaryCta={{label: 'Install from app store', href: '/install'}}
+ secondaryCta={{label: 'Read the docs', href: 'https://docs.conduction.nl/open-webui'}}
+ tertiaryCta={{label: 'View on GitHub', href: 'https://github.com/ConductionNL/open-webui-nextcloud'}}
+ iconColor="var(--c-blue-cobalt)"
+ icon={}
+/>
+
+
+
+
+
+
+
+
+
+Install from the Nextcloud app store. Pair with the Ollama sidecar for the model. SSO comes from the Keycloak sidecar. One workspace, no cloud round-trip.>}
+ primaryCta={{label: 'Install from app store', href: '/install'}}
+ secondaryCta={{label: 'Talk to a partner', href: '/partners'}}
+/>
diff --git a/src/pages/sidecars/openklant.mdx b/src/pages/sidecars/openklant.mdx
new file mode 100644
index 000000000..93d273491
--- /dev/null
+++ b/src/pages/sidecars/openklant.mdx
@@ -0,0 +1,47 @@
+---
+title: OpenKlant sidecar, Conduction
+description: Nextcloud sidecar wrapping the OpenKlant citizen-contact register. ZGW-compatible, installed from the app store.
+hide_table_of_contents: true
+---
+
+import {DetailHero, Section, SectionHead, CtaBanner} from '@conduction/docusaurus-preset/components';
+
+A Nextcloud sidecar that wraps OpenKlant. Run the official citizen-contact register next to your workspace, install from the Nextcloud app store, no separate cluster.>}
+ primaryCta={{label: 'Install from app store', href: '/install'}}
+ secondaryCta={{label: 'Read the docs', href: 'https://docs.conduction.nl/openklant'}}
+ tertiaryCta={{label: 'View on GitHub', href: 'https://github.com/ConductionNL/openklant'}}
+ iconColor="var(--c-blue-cobalt)"
+ icon={}
+/>
+
+
+
+
+
+
+
+
+
+Install from the Nextcloud app store. Pair with OpenConnector to route ZGW calls. Pair with Procest to drive workflows on citizen-contact events.>}
+ primaryCta={{label: 'Install from app store', href: '/install'}}
+ secondaryCta={{label: 'Talk to a partner', href: '/partners'}}
+/>
diff --git a/src/pages/sidecars/opentalk.mdx b/src/pages/sidecars/opentalk.mdx
new file mode 100644
index 000000000..381ddbaba
--- /dev/null
+++ b/src/pages/sidecars/opentalk.mdx
@@ -0,0 +1,47 @@
+---
+title: OpenTalk sidecar, Conduction
+description: Nextcloud sidecar wrapping the OpenTalk video-conferencing controller. GDPR-compliant meetings, end-to-end encryption, screen sharing.
+hide_table_of_contents: true
+---
+
+import {DetailHero, Section, SectionHead, CtaBanner} from '@conduction/docusaurus-preset/components';
+
+A Nextcloud sidecar that wraps OpenTalk. GDPR-compliant video conferencing, end-to-end encrypted, screen-sharing included. Installed through the AppAPI, run as an ExApp next to the workspace.>}
+ primaryCta={{label: 'Install from app store', href: '/install'}}
+ secondaryCta={{label: 'Read the upstream docs', href: 'https://opentalk.eu/'}}
+ tertiaryCta={{label: 'View on GitHub', href: 'https://github.com/ConductionNL/opentalk'}}
+ iconColor="var(--c-blue-cobalt)"
+ icon={}
+/>
+
+
+
+
+
+
+
+
+
+Install from the Nextcloud app store. GDPR-compliant, end-to-end encrypted, hosted next to the workspace your team already uses.>}
+ primaryCta={{label: 'Install from app store', href: '/install'}}
+ secondaryCta={{label: 'Talk to a partner', href: '/partners'}}
+/>
diff --git a/src/pages/sidecars/openzaak.mdx b/src/pages/sidecars/openzaak.mdx
new file mode 100644
index 000000000..01d7181e2
--- /dev/null
+++ b/src/pages/sidecars/openzaak.mdx
@@ -0,0 +1,47 @@
+---
+title: OpenZaak sidecar, Conduction
+description: Nextcloud sidecar wrapping the OpenZaak case-management API.
+hide_table_of_contents: true
+---
+
+import {DetailHero, Section, SectionHead, CtaBanner} from '@conduction/docusaurus-preset/components';
+
+A Nextcloud sidecar that wraps OpenZaak. Run the official Common Ground case-management API next to your workspace. ZGW-koppelvlakken, vanuit Nextcloud.>}
+ primaryCta={{label: 'Install from app store', href: '/install'}}
+ secondaryCta={{label: 'Read the docs', href: 'https://docs.conduction.nl/openzaak'}}
+ tertiaryCta={{label: 'View on GitHub', href: 'https://github.com/ConductionNL/openzaak'}}
+ iconColor="var(--c-blue-cobalt)"
+ icon={}
+/>
+
+
+
+
+
+
+
+
+
+Install from the Nextcloud app store. Route ZGW calls through OpenConnector. Drive cases through Procest workflows.>}
+ primaryCta={{label: 'Install from app store', href: '/install'}}
+ secondaryCta={{label: 'Talk to a partner', href: '/partners'}}
+/>
diff --git a/src/pages/sidecars/valtimo.mdx b/src/pages/sidecars/valtimo.mdx
new file mode 100644
index 000000000..7d753034b
--- /dev/null
+++ b/src/pages/sidecars/valtimo.mdx
@@ -0,0 +1,47 @@
+---
+title: Valtimo sidecar, Conduction
+description: Nextcloud sidecar wrapping the Valtimo BPMN workflow engine.
+hide_table_of_contents: true
+---
+
+import {DetailHero, Section, SectionHead, CtaBanner} from '@conduction/docusaurus-preset/components';
+
+A Nextcloud sidecar that wraps Valtimo. Run the open-source BPMN workflow engine inside your workspace, install from the app store, no separate cluster to operate.>}
+ primaryCta={{label: 'Install from app store', href: '/install'}}
+ secondaryCta={{label: 'Read the docs', href: 'https://docs.conduction.nl/valtimo'}}
+ tertiaryCta={{label: 'View on GitHub', href: 'https://github.com/ConductionNL/valtimo'}}
+ iconColor="var(--c-blue-cobalt)"
+ icon={}
+/>
+
+
+
+
+
+
+
+
+
+Install from the Nextcloud app store. Trigger workflows from Procest. Surface task lists in MyDash. Route inputs through OpenConnector.>}
+ primaryCta={{label: 'Install from app store', href: '/install'}}
+ secondaryCta={{label: 'Talk to a partner', href: '/partners'}}
+/>
diff --git a/src/pages/support.mdx b/src/pages/support.mdx
index 25c3ed6fc..babe37315 100644
--- a/src/pages/support.mdx
+++ b/src/pages/support.mdx
@@ -8,11 +8,11 @@ import {Section, SectionHead, HowSteps, HowStep, Card, Pill, HexBullet, HexNetwo
import {PARTNERS, BECOME_PARTNER} from '@site/src/data/partners-catalog';
export const TIER_PILL = {
- partner: {background: 'var(--c-cobalt-50)', color: 'var(--c-cobalt-700)'},
- certified: {background: 'var(--c-gold-500)', color: 'var(--c-cobalt-900)'},
- strategic: {background: 'white', color: 'var(--c-blue-cobalt)'},
+ host: {background: 'var(--c-cobalt-50)', color: 'var(--c-cobalt-700)'},
+ service: {background: 'var(--c-gold-500)', color: 'var(--c-cobalt-900)'},
+ certified: {background: 'white', color: 'var(--c-blue-cobalt)'},
};
-export const TIER_CARD_STRATEGIC = {
+export const TIER_CARD_CERTIFIED = {
background: 'var(--c-blue-cobalt)',
borderColor: 'var(--c-blue-cobalt)',
color: 'white',
@@ -54,17 +54,17 @@ export const TIER_PILL_BASE = {
eyebrow="How support works"
title="Three paths, picked by what you need."
align="stack"
- lede="Most teams never need anything beyond the docs. When you do, find a partner. For mission-critical or regulated rollouts, ask for a strategic engagement."
+ lede="Most teams never need anything beyond the docs. When you do, find a partner. For tenders or mission-critical rollouts, work with a Certified partner."
/>
Install from the app store. Read the docs. Ask in the public GitHub Issues or community Discord. Most MKB and pilot users never need anything else, and that stays free, forever.
- Running Conduction apps in production, or planning to? A partner deploys, configures, and supports the stack for you. Hourly, retainer, or project-based, whatever your contracting team prefers.
+ Running Conduction apps in production, or planning to? A Host or Service partner deploys, configures, and supports the stack for you. Hourly, retainer, or project-based.
-
- Mission-critical workloads, multi-app rollouts, or government tenders go through Strategic partners, co-developing with Conduction engineers and shaping the roadmap.
+
+ Public tenders, multi-app rollouts, and joint roadmap work go through Certified partners. They are trained and audited by us, and may co-respond to tenders with our products.
@@ -78,9 +78,9 @@ export const TIER_PILL_BASE = {
/>
- Partner
-
Implementation, on demand.
-
Self-certified on the apps. Installs and configures for one or two clients in a region or sector.
+ Host
+
Ships our apps to their customers.
+
Hosts and resells the open-source apps through their own channels. May offer Nextcloud-supported variants. No SLA with Conduction.
Conduction-certified per app. Joint go-to-market on tenders. Higher SLA on shared customers.
+ Service
+
SLA, third-line, named contacts.
+
Hosts and ships our apps with an SLA from Conduction. Third-line support from our engineers, named contacts, input on bug priority and feature requests.
-
Everything in Partner
-
Certified gold avatar
-
Co-branded case-study slot
-
Pre-release access (one minor)
+
Everything in Host
+
Official Nextcloud partner
+
SLA contract with Conduction
+
Direct line to third-line support
+
Roadmap and bug-priority input
-
- Strategic
-
Shared roadmap, shared release.
-
Conduction's deepest partners. Joint roadmap commitments and shared engineering, by invitation only.
+
+ Certified
+
Trained, audited, tender-eligible.
+
Trained by Conduction, audited annually, on the joint roadmap. Eligible to co-respond to public tenders alongside us.