Skip to content

release: promote development to main (academy tutorials + ConNext pivot + partners on app/solution pages) - #118

Open
WilcoLouwerse wants to merge 358 commits into
mainfrom
development
Open

release: promote development to main (academy tutorials + ConNext pivot + partners on app/solution pages)#118
WilcoLouwerse wants to merge 358 commits into
mainfrom
development

Conversation

@WilcoLouwerse

Copy link
Copy Markdown
Contributor

Summary

Promotes ~123 commits from development to main. Highlights:

  • Academy — new tutorials: spec-driven development (EN + NL), local Nextcloud, OpenSpec introductie, Hydra leerlijn, Claude Skills leerlijn, DeskDesk T5 (Integrate). Series-focus filter polish + locale-aware /academy links.
  • Blogthe-platform-moment rewrite (addressable-market segmentation) + DownloadPanel CTA polish.
  • Features — homepage hero pivot to ConNext, partners on every app & solution page, OpenRegister Showcase → MockScene, footer "Way of Work" entry, AI-crawler-friendly (llms.txt, robots.txt).
  • Solutions — added anonimiseren and openwoo; removed legacy-erp, software-catalog, woo.
  • Site — hide auto-generated "Footnotes" label, custom theme Error page, OG image refresh.
  • Copy — quality policy refreshed to January 2026; OpenTalk references removed from ConNext.
  • Chore — preset 3.20.0 bump, package-lock refresh, app download stats refresh.

174 files changed · +20,243 / −1,936

Test plan

  • Build passes on main after merge
  • Homepage hero renders with ConNext pivot
  • Academy index shows new tutorials in both EN and NL
  • App and solution pages render the partners block
  • /solutions/anonimiseren and /solutions/openwoo load; removed solution slugs 404 cleanly
  • static/llms.txt and static/robots.txt served at root

🤖 Generated with Claude Code

WilcoLouwerse and others added 22 commits May 21, 2026 17:47
…pec-introductie

# Conflicts:
#	src/css/site.css
…ijn-translations

# Conflicts:
#	src/css/site.css
* academy: add spec-driven development tutorial

Companion tutorial to the new
nextcloud-vue.conduction.nl/docs/architecture/configuration-over-code
page. Teaches the schema-first, manifest-second authoring workflow
without writing any Vue, and frames the same JSON contract as the
safe sandbox for AI agents, citizen developers, and the OpenBuilt
visual editor.

Slug 'spec-driven-development' so the docs site can link to
conduction.nl/academy/spec-driven-development.

* academy: reframe spec-driven tutorial around OpenSpec

The first draft framed "spec-driven development" as the JSON-manifest
authoring workflow. That was wrong. Rewrite around OpenSpec as
practised by Hydra: humans write Markdown specs (RFC 2119 +
GIVEN/WHEN/THEN) and ADRs, an AI agent implements to them, and a
sequential quality + gatekeeping harness validates the result.

Covers: the OpenSpec directory layout (living specs vs deltas), the
org-wide vs per-app ADR tiers governing samenhang, one-feature-one-spec,
the /opsx-explore thinking stance, the /opsx-apply implementer with
ADR-031 declarative business logic, the n8n/Windmill codeless path via
WorkflowEngineInterface, and the 13 mechanical gates + team-reviewer +
team-security review harness.

* academy: prerequisites, general ADR framing, opsx phase + flow diagrams

- Prerequisites rewritten: app-design-principles + manifest/schemas as
  knowledge prereqs, an LLM tool (Claude/Mistral/Codex/other), and the
  OpenSpec skills installed from the Hydra repo.
- ADR section generalised: describes the org/app two-tier pattern
  abstractly (no link to the org set), links OpenConnector's 16 real
  per-app ADRs as the concrete application-level example.
- New "workflow, phase by phase" section using the SetupSteps component
  for the opsx phase sequence (explore → scaffold → plan → apply →
  verify → archive), plus a stacked cn-domain-tree diagram showing the
  ADR tiers feeding every phase and apply outputting manifest + schema
  with optional code/workflows.
- New "feature request to delivered functionality" section using a
  cn-pipeline for the end-to-end flow.
- MDX fix: wrap `*`-bearing literals inside <code> in JS expressions so
  the asterisks aren't parsed as emphasis.

* build: bump docusaurus-preset to ^3.20.0 for SetupSteps; disable npm cooldown

The spec-driven tutorial uses the SetupSteps component, which only
exists in @conduction/docusaurus-preset >= 3.19.0. The site was pinned
at 3.17.0, so SetupSteps imported as undefined and crashed the MDX
render at runtime (passed webpack compile, failed in the browser).

- Bump @conduction/docusaurus-preset 3.17.0 -> 3.20.0.
- Temporarily disable the .npmrc min-release-age=1 supply-chain cooldown
  so 3.20.0 (published <24h ago) installs in CI. Tracked for re-enable
  tomorrow in #115.

* fix(academy): load diagram web components browser-only to fix SSG build

The top-level `import '@conduction/docusaurus-preset/diagrams'` ran
`class CnHex extends HTMLElement` at module-eval during static site
generation, where HTMLElement is undefined — crashing SSG for every
/academy/ path (ReferenceError: HTMLElement is not defined, cascading
to "Cannot access 'assets' before initialization" on tag/author pages).

BrowserOnly defers rendering, not the import. Fix: drop the top-level
import and require() the diagrams module inside each BrowserOnly render
function, so the side-effect only runs in the browser.
Supervisor won't dispatch retry:queued or rebuild:queued while needs-input or
fail labels are still set — add the explicit cleanup command sequence (incl.
HYDRA_LABEL_PREFIX variants) to tutorial 6 and cross-reference from tutorial 2.
docs(academy): OpenSpec introductie tutorial (parts 1 + 2)
…stance

Updated the local nextcloud tutorial for better use for non technical persons
The swizzled BlogPostPage in src/theme/BlogPostPage/index.jsx wraps
MDX in .content-detail-body and bypasses the default
BlogPostItem/Content component that adds the .markdown class. The
brand blockquote CSS in src/css/site.css is scoped to
.markdown blockquote, so raw `>` callouts in the Nextcloud tutorial
fell through to Infima's washed-out grey default and rendered without
border/background.

Convert all six callouts in both the EN source and the NL translation
to `:::info`, `:::note`, `:::tip`, `:::warning`, and `:::danger`
admonitions — the same pattern used by the Hydra, Claude Skills, and
OpenSpec tutorials, which never use raw blockquotes for this reason.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The installed version of @conduction/docusaurus-preset had drifted to
3.17.0 while package.json required ^3.20.0, so the SetupStep and
SetupSteps components used by the spec-driven-development tutorial
were missing from the resolved bundle, producing dev-server runtime
errors on the academy listing page. Running npm install pulls the
preset up to 3.20.0 and adds search-insights as a transitive
dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion NL translation for the EN canonical at
academy/2026-05-22-spec-driven-development/index.mdx, completing the
NL/EN pair so this tutorial appears on /nl/academy/ alongside the
Hydra, OpenSpec, and Claude Skills series.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…27)

The Conduction column was advertising '/academy?type=case-study' as
'Case studies', but case studies live on the Academy filter and
'Way of Work' is now the canonical 'how Conduction operates' surface
on docs.conduction.nl. Point readers at the handbook landing instead
of the academy filter.

- docusaurus.config.js: footer 'Case studies' -> 'Way of Work',
  href https://docs.conduction.nl/WayOfWork/way-of-work/.
- i18n/nl: add NL translation 'Werkwijze' for the new label.
We are no longer supporting OpenTalk as an integrated ExApp, so remove
it from the platform overview, the ExternalAppShelf, and the
app-downloads dataset. Rewrites the 'Why through the app store?'
paragraph to keep the Nextcloud Talk pitch without the OpenTalk-on-
Matrix alternative.
The "Typed data, surfaced everywhere" Showcase had a single AppMock
per tab plus an AgentTrace for the LLM tab. The first two tabs
underplayed the integration story by showing one app each in
isolation; the marketing punch is "this register data shows up
everywhere", which a multi-mock scene reads better than a single
app frame.

Mail and Files tab now composes:
  - openregister-metadata SidebarMock (md, centred)
  - nextcloud-mail and nextcloud-files WidgetMocks (sm, left edge,
    overlapping the sidebar)
  - openregister-activity WidgetMock (sm, behind the sidebar at z=0)

Windmill and n8n tab now composes:
  - openconnector-run-detail SidebarMock (md, centred)
  - openregister-activity and openconnector-runs WidgetMocks (sm,
    left edge)
  - procest-werkvoorraad WidgetMock (sm, behind the sidebar at z=0)

LLMs tab keeps the AgentTrace; the trace already reads as a mock
purpose-built for "watch the LLM working", and a scene
composition would lose that punchline.

Bumps @conduction/docusaurus-preset to ^2.6.0 (was ^2.1.0) for
WidgetMock + SidebarMock + MockScene + IntegrationIcon exports;
the components themselves landed earlier under that major.
* feat: show partners on every app and solution page

Drops a <PartnersFor> section directly below the "What it does" block
on all 16 app pages and all 5 solution pages, wired to
partners-catalog.js so /apps/<slug> and /solutions/<slug> stay in sync
with /partners from one source of truth. Replaces the hand-rolled
partner-logo strip on openwoo.

When the catalog has no partners for a subject (decidesk, doriath,
larpingapp, nldesign, openbuilt, pipelinq, procest, scholiq, shillinq,
softwarecatalog, zaakafhandelapp, anonimiseren, archief, mkb-workspace,
zaakafhandeling), the section collapses to the BecomePartner CTA only,
so every product surface keeps recruiting partners.

Site shims PartnersForApp / PartnersForSolution are a one-line MDX
drop-in that resolves the slug to the brand-cased name, runs the
matching reverse-lookup hook, and locale-keys the EN/NL copy.

Pairs with the <PartnersFor> preset component shipped in
@conduction/docusaurus-preset.

* chore(deps): bump @conduction/docusaurus-preset to ^3.18.0

Adds <PartnersFor> from the preset, required by the
PartnersForApp / PartnersForSolution shims on every app and
solution page in this branch.
…-dev

content(academy): add spec-driven-development blog post
i18n(academy): NL/EN translation pairs for Hydra, OpenSpec, Claude Skills
@MWest2020

Copy link
Copy Markdown
Contributor

Code Review — PR #118: release: promote development to main

Overview

Release-merge van developmentmain: 174 bestanden, +20.243 / −1.936. Bundelt ~123 commits — academy-uitbreiding (Hydra, Claude Skills, OpenSpec, DeskDesk T5/T6 series + i18n/nl mirrors), homepage-pivot naar ConNext, partners op alle app/solution pagina's, AI-crawler baseline (llms.txt/robots.txt/sitemap), solution-herstructurering, @conduction/docusaurus-preset major bump 3.3.0 → 3.20.0, Node 18 → 20, en een nieuwe PR-validation workflow. CI is groen (CodeQL pass, deploy / Build and validate pass).

Sterke punten

  • PR-gated validatie.github/workflows/validate-ai-baseline.yml draait de volledige build + AI-baseline check op iedere PR. Dat sluit precies het gat dat de inline comment beschrijft (centrale .github-workflow draait alleen op push). Belt-and-braces extra step is een nette redundantie.
  • Auditable comments — vrijwel iedere niet-triviale config-keuze (sitemap ignorePatterns, legalLinks, redirects, npmrc bypass, locale-gap voor nl) heeft een uitlegblok met waarom. Past goed bij de "boring, auditable" voorkeur.
  • Client redirects voor SEO-equity/over-ons, /openconnector*, /solutions/woo, /iso netjes afgevangen via @docusaurus/plugin-client-redirects. De keuze om de overige verwijderde URLs (componenten-catalog, legacy training) bewust te laten 404'en is gedocumenteerd.
  • Node 18 → 20 is consistent doorgevoerd: .nvmrc, package.json engines, en workflow node-version: '20'.

Aandachtspunten

1. min-release-age cooldown is uitgeschakeld (supply-chain)

.npmrc zet min-release-age=1 uit om @conduction/docusaurus-preset@3.20.0 te kunnen installeren vóór de 24h-cooldown verstreken was:

# TEMPORARILY DISABLED 2026-05-22 ... Re-enable tomorrow.
# Tracking issue: ConductionNL/conduction-website#115 (re-enable tomorrow).
# min-release-age=1
  • Vandaag is 2026-05-26 — "morgen" is 4 dagen geleden. De cooldown hoort allang weer aan te staan, óf de PR sluit met een commit die min-release-age=1 re-activeert. Niet mergen naar main met cooldown uit.
  • Bonus: de baseline stond op 1 dag, terwijl een 7-daags anti-yank-window aan te raden is. Overweeg meteen door te trekken naar min-release-age=7 bij re-enable.
  • De preset-versiesprong is groot (3.3.0 → 3.20.0, 17 minor releases). Aanbeveling: re-enable cooldown, draai daarna npm audit signatures en commit het resultaat van een verse npm ci --ignore-scripts als sanity check.

2. llms.txt is al stale bij landing

static/llms.txt beschrijft Solutions als:

"government use cases (WOO compliance, software catalog, zaakafhandeling, archief, legacy ERP migration, MKB workspace)"

…maar in dezelfde PR worden solutions/woo, solutions/legacy-erp en solutions/software-catalog verwijderd, en solutions/anonimiseren + solutions/openwoo toegevoegd. Sync de tekst (en de losse https://www.conduction.nl/iso link → die redirect nu naar /quality, dus de redirect vangt het op, maar canonical zou kloppender zijn). Een AI-crawler die llms.txt ophaalt krijgt nu een onjuiste eerste indruk van wat het bedrijf aanbiedt.

3. Redirect-dekking ontbreekt voor nl-locale

De comment in docusaurus.config.js benoemt het correct: client-redirects worden voor src/pages/*.mdx alleen voor de EN-route geëmit. Concreet: als ergens /nl/solutions/woo of /nl/iso is geïndexeerd of gedeeld (presentaties, partner-pages), gaan die naar een 404. Geen blocker — wel het overwegen waard om óf:

  • óf de NL-equivalent expliciet toe te voegen ({from: '/nl/iso', to: '/nl/quality'}),
  • óf een korte regel toe te voegen aan static/robots.txt/llms.txt over welke locale-paden gegarandeerd zijn.

4. start-script breaking change

package.json: start is docusaurus start --locale en geworden. Wie npm start gewend was om "default locale" te krijgen, krijgt nu altijd EN. start:nl is nieuw. Niet ernstig (intern team), maar wel vermelden in CHANGELOG en/of release notes — anders verliest iemand vijf minuten met "huh, NL render verschilt".

5. CI-workflow Node-versie hardcoded los van .nvmrc

.github/workflows/validate-ai-baseline.yml zet node-version: '20' letterlijk, terwijl .nvmrc ook 20 zegt. Bij een volgende bump (20 → 22) is dat twee plekken bijwerken, dus drift-risico. Triviale fix:

- name: Setup Node.js
  uses: actions/setup-node@v4
  with:
    node-version-file: '.nvmrc'

6. Voor release: dev → main PRs: graag ook GitHub release tag

Met 123 commits in één klap is git log op main straks lastig leesbaar. Overweeg een gh release create met dezelfde body als deze PR ná de merge — geeft auditors een single-page changelog.

Risico's

Risico Kans Impact Mitigatie
Supply-chain via uitgeschakelde cooldown laag hoog Re-enable vóór merge naar main; verifieer met npm audit signatures
Stale llms.txt schaadt AI-citaten hoog laag Sync solution-lijst in dezelfde commit
/nl/* deep-links 404 na rename laag laag NL-redirects toevoegen of bewust accepteren
Preset 3.3 → 3.20 verbergt regressie laag midden CI dekt het al via build + validator (groen)

Verdict

Approve onder voorwaarde: re-enable min-release-age (issue #115) en sync static/llms.txt vóór merge naar main. De overige punten zijn nits / opvolg-werk. CI is groen en de auditability is sterk — dit is het soort release-PR dat goed te reviewen blijft ook door iemand die de afgelopen weken niet meekeek.

🤖 Generated with Claude Code

The Docusaurus dev server only serves one language at a time, which
makes the navbar language switcher look broken locally. Document the
`npm run preview` / `npm run serve` path for testing both languages on
one port.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirror the structure of /quality (DetailHero + Showcase + Section + FAQ
from @conduction/docusaurus-preset). Lead with the SLA promises (uptime,
P1 response, security, capacity, maintenance, updates), then the full
SLA text. Genericized away from product-specific mentions ([product]
placeholder) and linked the OpenWoo SLA at openwoo.app/Product/SLA.

The Dutch version is the legally binding text.
…uality

Mirror DetailHero + Showcase + Section + FAQ from @conduction/docusaurus-preset.
Lead with the GDPR commitments (purpose-bound, no sharing unless necessary,
minimum retention, ISO 27001-aligned security, consent + transparency,
your GDPR rights), then the full privacy policy + DPIA + risk procedure.

The Dutch version is the legally binding text.
rubenvdlinde and others added 30 commits August 11, 2026 11:22
…169)

The post carries a '## Sources' endnotes section but was missing the
appsCta: inline frontmatter and the AppCrossLinks mount, so the
auto-appended CTA block rendered below the endnotes instead of above
them — the layout defect conduction-website#165 established the
convention to prevent. Every other sources-bearing post already
follows it; this one predates the convention.

Apps chosen for topic fit (open source cataloguing and procurement)
rather than the usual hermiq/openregister default.

Verified in the built HTML, not just the frontmatter: the CTA heading
renders at offset 27233 and the Sources heading at 37832.
…wolves post (#170)

The academy detail header was hardcoded off contentType (webinar ->
WebinarHero, opinion -> FeaturedCard, else ContentDetailHero). It is now
selectable per post through frontmatter, with that same mapping kept as
the default, so every existing post renders exactly what it did before:

  hero:         featured | detail | webinar | none
  heroTone:     cobalt | cobalt-dark | cobalt-deep | cobalt-50 | mint | orange
  heroIcon:     guide | case-study | webinar | tutorial | blog
  heroImage:    site-absolute path or absolute URL, replaces the hex icon
  heroImageAlt: alt text for heroImage
  heroEyebrow:  eyebrow label on the featured variant
  heroAccent:   orange | cobalt, featured variant only

Unrecognised values warn at build time and fall back, matching the
contract the 'ai' key already follows. 'hero: webinar' degrades to
'detail' when no YouTube id can be extracted, so a mistyped videoUrl
shows a normal header instead of an empty player.

Also adds the Dutch translation of 'Two wolves live in every public open
source project', at the same slug per the i18n convention, with the
untranslated engine post linked at its English URL.

Verified with five throwaway probe posts (since removed): variant
override in both directions, hero: none, heroImage/alt, and bogus values
warning + falling back. Regression-checked that the four posts with a
videoUrl still render WebinarHero and the opinion posts still render
FeaturedCard.
… redirect the old URLs (#171)

* content(academy): reclassify the hurt series as blog, add VideoEmbed, redirect the old URLs

Completes the in-flight work from an interrupted session (preserved on
wip/preserve-2026-08-11-hurt-reclassification) and adds the redirects it
still needed.

- The six 'This is going to hurt' parts move from contentType: opinion /
  slug: opinion/... to contentType: blog / slug: blog/..., with their
  cross-links between parts rewritten to match. Content is otherwise the
  post-#167 text plus two further info wells and a restructured
  al-Bashir passage.
- New src/components/VideoEmbed: an in-body 16:9 youtube-nocookie embed
  for supporting evidence in the prose, where WebinarHero is for posts
  whose video IS the page. Used once so far, in part 1.
- The six old /academy/opinion/... URLs were live for a day, so each gets
  a client redirect to its new /academy/blog/... address, and
  static/academy/blog/ gets the same stub treatment
  static/academy/opinion/ already has (-> /academy/?type=blog).

Verified on a full build: all six posts build at the new slug in both
locales, all 12 redirect stubs (6 x 2 locales) point at the new URLs,
the /academy/blog/ stub is emitted, the one <VideoEmbed> in source
renders one iframe, and the broken-link count is unchanged at 38 (the
pre-existing /about/# anchors), i.e. this introduces none.

* content(academy): keep the featured hero on the reclassified hurt series

The six parts were live for a day with the FeaturedCard hero they got
from contentType: opinion. Reclassifying them to blog would have swapped
that for the standard ContentDetailHero, changing the look of published
pages as a side effect of a taxonomy change. 'hero: featured' pins the
appearance while the taxonomy moves, which is what the configurable hero
was added for.

Verified on a full build: all six render FeaturedCard at their new
/academy/blog/ slugs; broken-link count unchanged at 38.
… post for OpenBuild (#172)

The post argues for funding the people already doing the work and
building your own capacity to consume and adapt the software, which is
what OpenBuild speaks to; the Software Catalog suggestion sat further
from the argument. Applied to both locales.

Verified on a full build: the openbuild CTA renders and softwarecatalog
is gone on both /academy/opinion/feed-the-wolf-that-ships and its /nl/
counterpart; broken-link count unchanged at 38.
Every academy hero drew its glyph from contentType, so all 52 tutorials
shared one icon and every blog post shared another. This adds a named
glyph vocabulary and gives each of the 74 posts its own.

- New src/theme/BlogPostPage/heroGlyphs.jsx: 37 line-art glyphs on one
  shared 1.6 stroke with round caps/joins, so an otherwise unrelated set
  reads as one family. They inherit currentColor, so they work on every
  hex tone.
- heroIcon: now resolves a glyph name first (wolf, pipeline, vault, …),
  then the five contentType names, so heroIcon: tutorial keeps working.
  An unknown name warns at build time and falls back to the contentType
  icon, so a typo shows the old generic glyph rather than an empty hex.
- Related-posts cards use the same glyph as the post they link to.
- All 74 posts assigned. Series share a glyph (build-an-app -> blocks,
  hydra -> pipeline, openspec -> spec), while individual pieces get
  their own: the two-wolves post gets a wolf, and the hurt series runs
  horse, coins, key, shield, brain, sunrise across its six parts.

Verified on a full build by checking each post's rendered HTML for a
signature path from the glyph its frontmatter names: 70 hex-hero posts
render their assigned glyph and the 4 webinar posts render their video
hero (WebinarHero replaces the hex, so their glyph only reaches the
related-posts cards). Zero heroIcon warnings, so every name resolved
rather than silently falling back. Broken-link count unchanged at 38.
…ost (#174)

Adds a section to part 3 on the clearest form of the pattern the post
describes: business software that used to run on machines you owned, now
being converted to subscription on a published timetable.

- Atlassian as the lead example: Server off sale in 2021 and out of
  support 15 February 2024, customers pointed at Data Center, and Data
  Center itself now off sale to new customers 30 March 2026, last
  renewals 30 March 2028, expiring 28 March 2029 — at which point the
  products and their Marketplace apps go READ-ONLY. Software on hardware
  you bought, in a building you pay for, switching to read-only on a
  date the vendor chose.
- Broadcom ending VMware perpetual licences weeks after the acquisition,
  Microsoft retiring perpetual Exchange for a Subscription Edition that
  a 2019 licence does not carry over to, and Intuit closing QuickBooks
  Desktop to new US subscribers.
- Keeps the post's existing habits: no villains (Atlassian gave years of
  notice), and an honest counterweight (Bitbucket DC carved out,
  self-managed GitLab/Odoo/Nextcloud still sold).
- Price-increase figures are attributed as reported ranges, not stated
  as audited numbers.
- durationMinutes 8 -> 10; seven new sources, ConNext stays last.

Also converts academy/2026-06-07-big-tech-de-deur-uit-1/index.mdx from
CRLF to LF — the only file in academy/ with CRLF endings. Verified
content-identical: 'git diff --ignore-cr-at-eol' reports 0 insertions
and 0 deletions, and its door glyph still renders after the rewrite.

Verified on a full build: the section and all seven source links render,
the sources list is exactly 25 entries, broken links unchanged at 38 and
zero heroIcon warnings.
…alling (#175)

Adds SharePoint to the self-hosted-software section of part 3. It is the
sharpest case in the section and it lands this summer:

- SharePoint Server 2016 and 2019 leave support in July 2026 with no
  extended-security-update programme, per Microsoft's own lifecycle page.
- The only supported on-premises successor is the Subscription Edition,
  which Microsoft's licensing guidance says runs only while you hold
  active Software Assurance — and if that coverage lapses you must
  UNINSTALL it and may only run SharePoint Server 2019, the version that
  just went out of support.

So the perpetual right the licence leaves you is the right to run
software nobody patches any more, which is the post's own End of
Ownership formula (paying the price of owning for the rights of renting)
made literal.

Both claims are cited to Microsoft directly — learn.microsoft.com
lifecycle and microsoft.com/licensing/guidance — rather than to reseller
summaries. The body says 'July 2026' rather than a specific day because
Microsoft's lifecycle table renders 7/15/2026 in Pacific time while the
date communicated everywhere else is 14 July; the month is unambiguous
and the sources carry the detail.

Verified on a full build: the paragraph, both source links and the
closing line all render; the sources list is 27 entries ending on
ConNext; broken links unchanged at 38, zero heroIcon warnings.
… passages in part 5 (#176)

Editorial layout change made by Ruben in the working checkout, brought
onto development on top of the hero-glyph work.

- 'Datacenter brain, walking weapons, lights-out factories…' moves up
  into the two-column split block, next to the war/Neuralink passage it
  concludes, instead of sitting alone beneath it.
- 'The natural objection to "one idiot away"…' moves into the split
  block under its own heading, so the section opens inside the column
  rather than above it.
- Two <br/> tags for spacing between the joined paragraphs.

Applied as a patch onto current development rather than copied wholesale,
because the checkout it came from predates #173 and its part 5 had no
heroIcon; 'heroIcon: brain' is preserved.

Verified on a full build: both moved paragraphs render inside a
hfaw-split block and appear exactly once each, both <br/> tags survive
MDX, the brain glyph still renders, broken links unchanged at 38 and
zero heroIcon warnings.
…dates

Bumps the npm_and_yarn group with 3 updates in the / directory: [brace-expansion](https://github.com/juliangruber/brace-expansion), [fast-uri](https://github.com/fastify/fast-uri) and [js-yaml](https://github.com/nodeca/js-yaml).


Updates `brace-expansion` from 1.1.14 to 1.1.18
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](juliangruber/brace-expansion@v1.1.14...v1.1.18)

Updates `fast-uri` from 3.1.4 to 3.1.5
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](fastify/fast-uri@v3.1.4...v3.1.5)

Updates `js-yaml` from 4.1.1 to 4.3.1
- [Changelog](https://github.com/nodeca/js-yaml/blob/4.3.1/CHANGELOG.md)
- [Commits](nodeca/js-yaml@4.1.1...4.3.1)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.18
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: fast-uri
  dependency-version: 3.1.5
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: js-yaml
  dependency-version: 4.3.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Both shipped as stubs with an explicit 'placeholder while we draft the
full version' line in the body: the parhelion case study (pending the
customer's permission) and the OpenTelemetry drain-processor note.
Neither had been filled in, so they were publishing an empty promise.
Docusaurus styles post bodies with direct-child selectors (.markdown > p),
so anything nested inside a wrapper div in an MDX post is a grandchild and
matches nothing: paragraphs, headings and lists all computed margin 0 and
ran together with no gaps.

Part 5 had been working around this with <br/> between paragraphs, which
produces a line break rather than a paragraph: the gap was whatever the
line height happened to be, and the markup claimed a structure it did not
have. Both spacers are removed and the text is real paragraphs again.

Applies to all six academy posts using these wrappers. Only inherited
typography is restored, so each post's own layout rules still win.
A La Suite design language ported onto Nextcloud in one working session:
no fork, no patch set, no new applications. Walks the three sovereign
workspace programmes (openDesk, La Suite, MijnBureau) through vision,
architecture and portal, then the merge itself, the defects it surfaced,
and why a shared finish is the precondition for a shared app store.

Seven hand-authored SVG diagrams, colour-coded by component provenance.
Every claim sourced; the widely repeated La Suite user figures and the
732M savings claim are deliberately absent because they did not verify.

Also carries two site-wide fixes to the academy layout:
- the AI disclosure rendered BOTH the light and dark treatment, because
  the display:none hiding one lost on specificity to the sizing rule.
  Since *-white-transparent.svg is a white disc with near-black letters,
  the 'hidden' mark showed as a second set of dark AI letters. Now one
  image, its treatment chosen in JS, so it cannot double again.
- the mark's copy says what the assistance was for and links to /ai.
…_and_yarn-c724bfd08b

chore(deps): bump the npm_and_yarn group across 1 directory with 3 updates
docs(apps): add an App health page with per-branch Code Quality status
…L + EN (#181)

* content(apps): rewrite all 21 product pages for the business owner, in NL and EN

The reader of /apps/<slug> runs a business of 10 to 500 people and has
never heard of OpenRegister. Every page now follows the three product-page
rules added to the identity kit (taalgebruik §16.1): their vocabulary, not
ours; every example proves added value (today / instead / so); every scene
grounded in shipped functionality.

What changed on every page
- "Three ways <App> earns its place" is now the platform trio in the
  reader's words: Automate (event-driven flows), Make it yours (change any
  page and field, keep changes through updates), Talk to it (the assistant
  that acts in the app, with per-assistant permissions and approval).
  Card eyebrows are verbs, not product names; CTAs point outward, never
  back at the page.
- "How <App> plugs into the workspace" rebuilt from verified integrations
  only (mail, contacts, calendar, files, Talk, Deck), with the value each
  one adds. Claims not shipped were softened or removed (recorded in the
  research dossier so nobody re-inflates them).
- Hero: canonical taglines from the identity kit, Beta chip on every app,
  install / partner-demo / GitHub CTA trio, AppMock illustration on all
  pages (eight new variants ship in @conduction/docusaurus-preset 3.29.0;
  those heroes render the fallback frame until the site bumps).
- Voice: no em-dashes, no Dutch nouns in English copy, no platform
  internals in body copy, sentence case, short sentences.
- Eight thin stubs (doriath, hermiq, portaliq, scholiq, shillinq, planix,
  hrmq, openbuild) gain the full section set. zaakafhandelapp shrinks to a
  successor page pointing at Procest. Developer surfaces (MCP tool shelves,
  the OpenRegister "full spec" grid) leave the marketing pages.
- All 21 Dutch pages regenerated as full translations (je/jij, fixed NL
  labels, canonical NL taglines).

Every page passes the mechanical gate (MDX compile, em-dashes, jargon,
CTA labels, self-links, u-form for NL) and both locales build clean.

* build(preset): bump @conduction/docusaurus-preset to ^3.31.0

First release since 3.28.0 (npm trusted publisher pointed at a deleted
workflow filename; fixed today). Brings the eight AppMock hero variants
(hermiq, portaliq, scholiq, shillinq, doriath, planix, hrmq,
app-versions): zero product pages render the fallback frame now, was
seven. Signed provenance on the release.
…carousels (#182)

Applies the product owner's live review of the first rollout, on preset
3.32.0 (FlowMock, BuildMock, KanbanMock, LeafMock, widget carousel, new
hero layout).

Every page: "Four ways <App> earns its place" with a new Portaliq
self-service card tailored per app; Automate cards show the animated
flow; Make-it-yours cards show the OpenBuild drag-and-drop animation;
workspace showcase items get distinct leaf atomics; the widget shelf
becomes "We ship the widgets you need", a slow carousel carrying every
widget the app ships (from the manifests), ending in the
design-your-own line; pairing is OpenBuild / Hermiq / LaunchPad only;
partners sit at the bottom; install CTAs deep-link to each app's own
app-store listing where one exists; demo CTAs deep-link to
/partners/?app=<slug>, which now pre-filters the directory; every
sibling-app mention is a link; assistant scenes are spoken, not typed.

PipelinQ additionally gains its customer-centre story (tickets,
incidents, questions, complaints), the quotes-contracts-invoices line
with Shillinq, and the Humaniq hours-to-invoices reporting line, and
its hero shows the animated pipeline.

Honest exceptions: doriath keeps three ways (a zero-knowledge vault has
no client self-service); portaliq keeps the trio (it IS the story);
nldesign's shelf keeps its truthful token framing (zero widgets);
hermiq/portaliq/planix/hrmq keep the generic install link (no store
listing); launchpad too (its listing still sits under retired mydash).

Both locales pass the mechanical gates (42/42) and the full dual-locale
build is clean with zero hero fallbacks.
)

* test(e2e): product-page skeleton suite + partners deeplink polish, preset 3.33

Adds e2e/product-pages.spec.js: 42 per-page skeleton tests (renders,
hydrates, hero never falls back, the ways-section and carousel controls
where each page has them, demo deeplink), a carousel-operates test
(pause = zero drift, nudge moves one card), and partners deeplink tests.

Partners page: a known ?app= slug is now always acknowledged — filtered
with an escape hatch when partners carry the app, a get-started note
when none do yet (PipelinQ today); unknown slugs are ignored entirely.

Bumps the preset to ^3.33.0 (carousel controls + auto-panels, kanban
alignment and € KPI tiles).

* build(preset): bump to ^3.35.0 — every atomic animated

3.34.0 animated the high-traffic mocks (Portaliq, leaves, AgentTrace,
LaunchPad set, FlowMock approval hold) and 3.35.0 finished the fleet:
storytelling loops on every app mock, the HRMQ and DeciDesk content
reworks, widget list-refresh and sidebar animations, and the committed
animation roadmap. Full e2e suite: 109/109 against this build.
#184)

Applies the 2026-08-21 fleet renaming board to /apps, /connext, the
academy, solutions, partners, footer, i18n and llms.txt. Slugs, URLs,
subdomains, GitHub refs, i18n keys and filenames are byte-identical;
per-app detail pages carry a one-time (formerly X)/(voorheen X) note.
Pages reading names from @conduction/docusaurus-preset pin the new name
until the preset's apps-registry is renamed upstream.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
… rename (#185)

* fix: Planix becomes Planninq (not Planiq); correct Pipelinq casing; pin names in e2e

PlanIQ is Anaplan's trademark in the same planning category, so the app
takes the -inq form, consistent with Pipelinq/Shillinq/Larpinq. The repo
is already renamed to planninq, so the GitHub CTA now points there.

PipelinQ was spelled with a mixed-case Q in 56 places against the app's
own lowercase-q brand; brand/taalgebruik.md documented the wrong rule and
is corrected too.

Adds e2e/product-names.spec.js: the rename is display-name-only, so a
regression does not 404 - the page renders perfectly with the wrong
brand, and nothing else in the suite would notice. Each app is asserted
by the presence of its new name (an empty page passes an absence check),
each route is asserted to still resolve on its old slug, and the apps
that were deliberately not renamed are pinned so a future sweep cannot
overreach.

* fix(e2e): remove the space-invaders flake at its two sources

The suite failed on roughly one run in thirteen, a different test each
time, always with a mangled command or a missing echo. Two races, both
in the test's synchronisation rather than in the app:

1. openBannerFromFooter waited for the terminal input to be attached,
   but the keydown handler lives on window and is registered by an
   effect that React runs after paint. Attached is not listening, so
   keystrokes in that gap were dropped. It now proves the shell is
   listening - probe a character, wait for the buffer to echo it, clear
   it - instead of assuming it.

2. Three assertions read terminalText() as a one-shot snapshot right
   after typing, racing the render that paints the echo. They now use
   retrying expect(locator).toContainText, which asserts the same thing
   and waits for it.

65 consecutive space-invaders runs green, against 3 failures in 39
before; full suite 131 passed.
Every www.conduction.nl deploy has failed since ~15 Aug. The cause is not a
missing secret — it is a NAME MISMATCH. The workflow asked for
CLOUDFLARE_API_TOKEN; the org secrets are CF_API_TOKEN and CF_ACCOUNT_ID, and
ConductionNL/.github's own deploy-docs.yml already maps them under exactly
those names.

wrangler therefore received an empty token on every run.

Why it stayed invisible: the deploy step is `if: github.event_name !=
'pull_request'`, so PR checks were green throughout and only the post-merge
push run failed — the same absence-as-success shape as a skipped check.

CLOUDFLARE_ACCOUNT_ID is kept as a fallback for the account id, since that one
does exist at repo level.

Header comment and docs/DEPLOYMENT.md corrected to name the real secrets.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…oduction on development (#187)

* ci: list the Pages projects the token can reach, and gate production on development

This workflow has never deployed successfully — 30 of 30 runs red — so every
merged content PR since #181, including all the -iq display names, was built
and thrown away. www.conduction.nl still serves a build with none of them.

Merging #186 fixed the secret NAME: wrangler now authenticates and resolves the
Conduction account. Cloudflare still rejects
/accounts/*/pages/projects/conduction-website with Authentication error 10000,
while the same CF_API_TOKEN deploys the conduction-docs project from
ConductionNL/.github. A valid token that cannot see one project means the
project name or the token's project scope is wrong — and `conduction-website`
is the only name this workflow has ever used.

`pages project list` prints the names the token can actually reach, so the next
run answers the question from the log instead of from the dashboard. It is
continue-on-error because a diagnostic that can fail the build is a second
failure mode, not a diagnosis.

The deploy step is also gated on `development` now. It deploys to the
production `main` alias, so a workflow_dispatch from any branch published that
branch to www.conduction.nl.

* ci: ask Cloudflare what this token can see, Pages and Workers both

`pages project list` — the LIST endpoint, with no project name in it — returned
the same Authentication error 10000. So the failure is not a wrong project
name: the token cannot read Pages on this account at all.

Which fits the one place the same token demonstrably works. docs.conduction.nl
is not a Pages project: ConductionNL/.github deploys it as a WORKER
(`wrangler deploy`, `name = "conduction-docs"`, `[assets]`), and its own
error message names the permission it needs — Account > Workers Scripts > Edit.
A token with Workers Scripts and without Pages behaves exactly like this one.

If www.conduction.nl is likewise a Worker, this workflow has been calling the
wrong API since it was written, and the fix is a wrangler deploy rather than a
new token permission. This step asks Cloudflare directly — token permission
groups, Pages projects, Worker scripts — instead of guessing between the two.

* ci: probe zones and worker custom domains too

The first probe answered the main question: Pages returns Authentication error
10000 on the LIST endpoint, so the token has no Pages permission at all, while
Workers returns ok and lists 42 scripts. Every docs site in the fleet is a
WORKER named <app>-docs — openconnector-docs, procest-docs, docudesk-docs and so
on — which is why deploy-docs.yml works with this same token and this workflow
never has.

There is no conduction-website worker in that list, and www.conduction.nl is
nonetheless live. So either it is a Pages project this token cannot see, or a
worker whose custom domain says so. This probe asks which, and also whether the
token can read DNS zones — that decides whether the 14 missing <newname>.conduction.nl
docs hosts can be created from CI or need the dashboard.

* ci: name the real cause when the Pages deploy is refused

Measured, after 30 failed runs out of 30:

  /accounts/*/pages/projects   -> Authentication error 10000
  /accounts/*/workers/scripts  -> ok, 42 scripts
  /zones                       -> ok

CF_API_TOKEN carries Workers and zone-read and no Pages permission at all. It is
not a wrong project name: the Pages LIST endpoint names no project and is
refused too. `conduction-website` is real — it exists and serves
www.conduction.nl, conduction.nl, academy., commonground. and connext.

This is also why ConductionNL/.github publishes docs.conduction.nl with the SAME
secret: every docs site in the fleet is a WORKER (`wrangler deploy`, name
`<app>-docs`), and Workers is exactly what this token can do.

The fix is one dashboard edit — add 'Cloudflare Pages: Edit' to CF_API_TOKEN —
so the preflight states that in the log rather than leaving the next reader with
"Authentication error", which reads as a bad secret and is not one.

The deploy is also gated to `development`: it publishes to the production
`main` alias, so a workflow_dispatch from any branch replaced www.conduction.nl
with that branch.

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
* feat(apps): move the twelve renamed app pages to their -iq paths

The display names moved in #184/#185; the URLs did not. /apps still linked to
/apps/openbuild, /apps/procest, /apps/hrmq and nine more, so every product page
read "Buildiq" at an /apps/openbuild address.

Twelve pages renamed, every internal href rewritten (131 of them across the app
pages, /connext, /commonground, the solutions pages, support and llms.txt), and
twelve client-side redirects added so the published URLs keep working.

THE PRESENTATION KEYS IN src/data/apps-catalog.js DELIBERATELY DID NOT MOVE, and
that is the whole care in this change. They are the join key against `apps[].id`
in data/app-downloads.json — which is the NEXTCLOUD APP-STORE id, not the GitHub
repo slug, and the store still lists the old ids. getApps() filters out any entry
with no store match and no downloads, so renaming those keys would have dropped
all twelve apps off /apps entirely, with no error and no empty state — just a
shorter grid. Verified after the build: still 21 apps, now on the new paths.

The redirects are also what keeps @conduction/docusaurus-preset working. Its
apps-registry still ships the old /apps/<old> hrefs and feeds <AppCrossLinks/>
and the academy product filter; those now resolve through a 301 rather than
breaking, until the preset ships a release with the new paths.

Build green, all 15 AI-baseline checks pass, and every old path verified
redirecting to its new one in the built output.

* test(e2e): follow the app pages to their new paths, and pin the redirect

The `build` job failed with 11 Playwright failures, all the same shape:
product-pages.spec.js iterates a map keyed by app slug and visits
`/apps/<slug>`. Those keys were still the old slugs, so every one of them now
lands on a redirect stub whose content assertion cannot match.

product-names.spec.js needed the opposite treatment. It asserted, in as many
words, that "the rename must not have moved the URL" — correct when only the
display name had moved on 2026-08-21, and false since the paths moved. A test
that pins the old behaviour after the behaviour deliberately changed protects
nothing; it just fails until someone edits it.

So it now asserts the REDIRECT rather than the absence of one, and checks BOTH
sides: `/apps/<old>` reaches `/apps/<new>` and shows the product name, and
`/apps/<new>` serves it directly. That is the behaviour worth defending — the
old URLs were published, and @conduction/docusaurus-preset's apps-registry
still points at them from <AppCrossLinks/> and the academy product filter.

Verified locally: 56 passed across both specs.

* fix(apps): move the openconnector page too, and its Dutch translations

Two gaps in the path move, both found by the e2e suite rather than by reading.

openconnector -> integriq was missing from the rename set entirely. The app was
renamed like the other twelve, so the spec's map expected /apps/integriq — which
did not exist. Page moved, 20 hrefs rewritten, redirect added.

And the Dutch pages live in a parallel tree, i18n/nl/docusaurus-plugin-content-pages/apps/,
which still held the old filenames. Docusaurus falls back to the English page
when a locale has no file at that path, so /nl/apps/buildiq/ silently rendered
the English copy — no error, no missing page, just a Dutch route serving English.
All thirteen moved, 115 hrefs rewritten across the locale tree.

Also corrected an assumption the spec had inherited from me: it derived the
partners deep link and the AppMock illustration from the PAGE SLUG. Those two
are keyed on the APP ID, which deliberately did not move — `?app=` filters the
partners directory and <AppMock app> resolves an illustration out of
@conduction/docusaurus-preset, and both only know the old ids. Moving them would
have filtered on a value the partners page has never seen and rendered the
"Unknown app" fallback frame, which the assertion two lines above explicitly
forbids. The spec now carries an explicit PARTNER_APP_ID map saying so.

81 passing across both specs, from 25 failing.

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
GitHub is the only host for this org; Codeberg was a mirror and is not
used, including for issues. Many of these URLs also still carried
pre-rename repo names, so a plain host swap would not have been enough.

Repointed 272 URL occurrences across 80 files (en + nl in lockstep):

- Org/repo links       codeberg.org/Conduction/<slug>
                       -> github.com/ConductionNL/<new-slug>
- Pre-rename slugs     nldesign->thematiq, doriath->keepiq,
                       docudesk->filinq, openconnector->integriq
- File links           /src/branch/<B>/<p> -> /blob/<B>/<p>
- Directory links      /src/branch/<B>/<dir> -> /tree/<B>/<dir>
- Raw links            /raw/branch/<B>/<p> -> /raw/<B>/<p>
- SSH clone URLs       git@codeberg.org:Conduction/x.git
                       -> git@github.com:ConductionNL/x.git
- Link text            "X on Codeberg" -> "X on GitHub" only where the
                       label sat on a URL that moved

Issue-number decision: Codeberg issue numbers do NOT correspond to
GitHub issue numbers, so rewriting the host would have pointed readers
at a real but unrelated GitHub issue — worse than a dead link. The five
decidesk issue links (#44/#45/#71/#72) therefore lost their URL and kept
the fact as plain text, marked as pre-migration Codeberg numbering.

Every target was verified to exist before linking: 15 public URLs
checked with curl (all 200) and 16 private-repo (hydra) paths checked
with `gh api`. Two links whose targets no longer exist were retargeted
at the containing directory rather than at a guessed filename
(hydra docs/operations/retry-and-rebuild.md, and the app template's
.github/workflows/release.yml, now release-beta/release-stable).

Two branch corrections found during verification: openregister's
calendar-provider spec and integriq's openspec/architecture live on
`development`, not `main` as the Codeberg links claimed.

Deliberately left (see PR body): the generated data/app-downloads.json,
the Codeberg API collector script, the two-forge tutorial with parallel
Codeberg/GitHub tabs, the Codeberg-account onboarding step in
workstation tutorial 2, a hydra record sample whose repo field sits
beside an issue number, and an author's personal Codeberg profile.
…180)

* content(apps): rewrite the eight product pages for the reader, not for us

Applies the product-page voice rule: **their vocabulary, not ours**. The reader
runs a business of 10 to 500 people and has never heard of OpenRegister.

DocuDesk's description was "Generate, anonymise, sign, and template documents on
Nextcloud. Templates ship for the most-used Dutch government documents." — three
platform nouns and a verb ("template") that is not one, describing the product
by how it is built. It now says what the reader gets: contracts and letters fill
themselves from your records, clients sign on their phone, personal data is
blacked out for you.

The same pass across DocuDesk, Hermiq, Launchpad, OpenBuild, OpenRegister,
Pipelinq, Shillinq and SoftwareCatalog. Platform names now appear in exactly one
place per page — the CTA label linking to that app.

Each page also pulls its name and metadata from `getApp(...)` rather than
repeating the string in the hero, the breadcrumb and the CTA. A product renamed
in three places is a product renamed in two of them.

* content(academy): Vae Victis, parts 1-3

Three new posts — Pax Silica, Bread and Butter, Alea Iacta Est — plus the
Frankendesk logo the earlier post referenced without shipping, and edits to
parts 2, 3 and 6 of "This Is Going To Hurt".
…video (#168)

* feat(site): app descriptions in AppCrossLinks (interim), VideoEmbed component, first video in hurt part 1

- apps-registry-interim + AppCrossLinksInterim via webpack replacement (same mechanism as hermiq-glyph-interim): every app gets a one-line description and tags in ONE site config file, and the About-this-app card renders the description
- VideoEmbed: responsive YouTube/Vimeo embed (nocookie/dnt), safe fallback link for unknown hosts
- Part 1 embeds the Amazon warehouse-robot video beside the robots paragraph as the pattern example

* fix(site): interim AppCrossLinks imports via relative node_modules paths (preset exports map blocks deep subpaths)

* fix(site): Blogs chip includes opinion posts (blog as umbrella type); Opinions chip narrows

* fix(site): one reader-facing category — drop the Opinions chip, Blogs matches blog+opinion; opinion stays the internal flavor for eyebrow/hero

* feat(site): blog posts get the opinion presentation — FeaturedCard hero with type eyebrow for both blog and opinion

* feat(site): deprecate the opinion content type — all 9 posts retyped to blog, umbrella filter reverted, hero tolerates stragglers

* content(academy): Monday blocks beside the closing text in all six parts; AI-written-code info well in part 1

* content(academy): part-5 layout — goalposts scorecard list + 1980 test, al-Bashir case well, alarm wells and hardware list beside the text

* feat(site): migrate opinion/ slugs to blog/ with no-chain redirects; feed-the-wolf review round — wolves HexCards, Gaia-X and saboteur wells, Monday block

- All 9 essay slugs move to /academy/blog/*; every /academy/opinion/* URL 301s straight to the final slug (no chains), earlier pre-opinion URLs re-pointed
- Static stubs: /academy/opinion/ and /academy/blog/ both land on /academy/?type=blog
- Zero residual opinion links in content; connext CTA updated
- feed-the-wolf gets the current furniture: hfaw style block, code/policy wolf HexCard pair, Gaia-X-in-numbers and saboteur's-checklist wells beside the text, closing split with a What-you-can-do-on-Monday block

* fix(mdx): repair two posts my conflict resolution structurally broke

The build failed with "Expected a closing tag for `<div>`" on two academy
posts. That damage was mine: I resolved 16 conflicts with a regex that keeps
the development side of each hunk, which is fine for a frontmatter line and
wrong for a hunk that straddles JSX block boundaries. It left an orphan
`<div className="hfaw-split">` in one file and a `</aside>` where a `</div>`
belongs in the other.

Both source versions were structurally sound; only my merged result was not.
So each file is restored from whichever side actually carries the content:

  - part-5-the-singularity  <- development (348 lines, newer than the 345 here)
  - governance-nix-implosion <- this branch (179 lines vs development's 125,
    since the branch is where the added content lives), with its frontmatter
    aligned to development's contentType: opinion + heroIcon: wolf

Swept every other .mdx for the same damage. Six files report unbalanced div
counts, and all six are byte-identical to development — they use self-closing
`<div ... />`, which a naive open/close count reads as a missing close. Not
regressions, and not touched.

* fix(routes): put the opinion posts back on the opinion/ slugs

The build failed on a redirect to a path that does not exist:

  These paths are redirected to but do not exist:
  - /academy/opinion/feed-the-wolf-that-ships/

Cause is mine. The previous commit resolved the conflicts toward development —
which keeps `opinion` as a live content type and redirects to /academy/opinion/*
— but restored one post's body from this branch, and that side still carried
`slug: blog/feed-the-wolf-that-ships`. Config pointed at opinion/, the post
answered on blog/, and Docusaurus refuses to emit a redirect to a route nothing
serves.

Rather than fix the one path the build named, I compared every academy post's
slug against development. Three disagreed, all the same way, and the other two
would have failed the same build on the next redirect it checked:

  the-platform-moment                     blog/ -> opinion/
  government-open-source-needs-an-engine  blog/ -> opinion/
  feed-the-wolf-that-ships                blog/ -> opinion/

All three now match development. Also restored `appsCta: inline` on the wolf
post, which the body swap dropped.

* fix(routes): retarget the three moved posts, and drop only their self-redirects

Moving the three posts to opinion/ left the config redirecting to blog/ paths
that no longer resolve, so the build refused: "These paths are redirected to
but do not exist".

Two changes, both scoped to the three posts that actually moved:

  - retarget /academy/blog/{slug} -> /academy/opinion/{slug}
  - drop `from: /academy/opinion/{slug}` entries, which now point away from
    the canonical page rather than toward it

My first attempt removed every `/academy/opinion/* -> /academy/blog/*` entry,
which was wrong: the six `this-is-going-to-hurt-part-N` posts really do live
at blog/, so those redirects are correct and carry the old opinion URLs. That
pass would have 404'd all six. Reverted and re-applied to the three moved
posts only.

Verified with a full local `docusaurus build --locale en`: "[SUCCESS]
Generated static files", no invalid-redirect error and no MDX failure.
…it (#191)

* feat(connext): a downloadable ecosystem compose and the tutorial for it

Adds static/connext/connext-compose.yml — one file that brings up the eleven
apps the Connext page names, on one Nextcloud, enabled in dependency order —
and an academy tutorial that walks through it.

NOT A DEVELOPMENT ENVIRONMENT, AND IT CANNOT BE MADE INTO ONE

Nothing is bind-mounted. Nextcloud installs and updates an app by DELETING its
directory and extracting a fresh archive over it, so pointing that at a checkout
means an app-store update deletes the checkout. Measured 2026-08-27 on a
development machine: \OC\Updater::upgradeAppStoreApp fired on a container
restart and removed every top-level file from a bind-mounted app, including its
.git directory.

Release archives rather than clones, for a second reason too: an archive is a
COMPLETE app carrying vendor/ and built js/, while a clone carries neither — and
a Nextcloud app with no vendor/ does not fail loudly. It warns once and keeps
loading, so it looks installed while every service needing a dependency is
absent.

THREE THINGS THIS FILE LEARNED THE HARD WAY, EACH DOCUMENTED IN PLACE

1. Compose interpolates $VAR inside `configs.content`, so every shell variable
   in an inline script is blanked before the file is written. Hence `$$`.

2. The GitHub releases list is NOT ordered by creation date, and the "latest
   release" endpoint excludes prereleases entirely. Taking the first entry
   installed an older openregister than the leaf apps needed, and the failure
   surfaced as a missing CLASS in a different app rather than as a version
   complaint. Sorted by created_at explicitly.

3. An app's repo name, its Nextcloud app id and its archive filename are three
   different names that moved at different times. thematiq was installed as
   `nldesign` on the assumption the id had not moved with the repo; it had, and
   Nextcloud refused it with "appinfo file cannot be read". The <id> in the
   RELEASED archive is the only authority — a local checkout can sit on a branch
   where it has not moved yet.

The tutorial is English, matching the existing per-app tutorials, and includes
the verification step that distinguishes "the page loaded" from "the install
worked" — an empty directory API reads as "no federation peers" when it
actually means the register configuration was never imported.

* fix(academy): use markdown links, not angle-bracket autolinks

MDX parses `<http://…>` as the start of a JSX tag, and the build fails with:

  Unexpected character `/` (U+002F) before local name, expected a character
  that can start a name, such as a letter, `$`, or `_`

Three table rows carried the autolink form. Converted to `[text](url)`.

Verified by building the site locally rather than round-tripping CI: the build
now succeeds, /academy/set-up-the-connext-ecosystem/ renders, and
/connext/connext-compose.yml is served (12,488 bytes) so the download URL the
tutorial gives actually resolves.
…installs (#192)

The tutorial's expected `apps present:` line still listed `nldesign`. The
compose was corrected to install that app under `thematiq` — its released
appinfo/info.xml declares `<id>thematiq</id>`, and Nextcloud refuses an archive
whose id does not match the directory — but the tutorial's transcript was not
updated with it.

Copied verbatim from a real run rather than retyped:

  ==> apps present: filinq hermiq integriq launchpad opencatalogi openregister
      pipelinq portaliq shillinq thematiq zaakafhandelapp

A sample output that disagrees with the tool is worse than none: a reader who
compares the two has no way to tell which of them is wrong, and the honest
answer here was "the documentation".
* docs(academy): a Run-locally tutorial for each of the 21 apps

Each tutorial stands one app up from its own demo compose file, on real
Nextcloud with OpenRegister behind it, and ends with a content check rather
than a status check -- Nextcloud serves its page shell before an app decides
whether it has anything to render, so an app URL returns 200 even when it
resolves to nothing.

Grounded in the app's real docs/features.json rather than an invented feature
tour. The two apps whose features.json is empty (portaliq, keepiq) get a
hand-written short list instead of an empty section.

Also corrects the Connext ecosystem tutorial's apps: list. It named integriq,
filinq and thematiq, and the website's app registry is pre-rename -- getApps()
resolves a slug via APPS_REGISTRY and does .filter(Boolean), so an unknown
slug is dropped silently, with no error and no chip. Those three were being
discarded. They are now openconnector, docudesk and nldesign, which is what
the registry answers to, and zaakafhandelapp is added since the ecosystem
compose installs it.

* docs(academy): add the truncate marker and drop feature titles from Outcomes

73 of the 98 academy posts carry a {/* truncate */} marker; these did not, so
the blog list would have rendered each one in full. Adds it after the intro,
where the rest of the corpus puts it.

Also replaces the Outcomes that were built from features.json titles. A raw
title rendered as an outcome reads as "Flow Authoring." -- the name of a
feature, not something the reader walks away having done. The feature list
stays where it belongs, under "what to look at first".

* docs(academy): point Thematiq at the page it actually serves

Thematiq declares <admin-section>theming</admin-section> and ships no app
route, so /apps/thematiq/ answers 404 even when authenticated. Measured on a
booted demo. The tutorial now opens Settings -> Administration -> Theming,
which answers 200.

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants