Skip to main content

Public site blocks — @conduction/nextcloud-vue/public

A second entry point, for components that must render where there is no Nextcloud.

import { siteBlockRegistry, CnSiteHero } from '@conduction/nextcloud-vue/public'

What it is for​

A portal served from a municipality's own domain — portaal.gemeente.nl, not cloud.gemeente.nl/apps/… — has no OC global, no session, no CSRF token and no translation bundle. Anything reaching for those fails in a browser, on a live page, not at build time and not in a Nextcloud-hosted test.

The main entry cannot serve that host. Measured on the published build:

packagefiles referencing it
@nextcloud/axios154
@nextcloud/router157

That is correct for an app rendering inside Nextcloud. It is fatal outside it.

Why these are separate components, not re-exports​

Re-exporting the existing widgets was the original plan. A direct import check supported it — 12 of 13 candidates had no @nextcloud/* import of their own. A transitive check, following relative imports through the tree, inverted the result completely:

widgetreachable Nextcloud runtime
CnTextWidget@nextcloud/l10n
CnHeaderWidget@nextcloud/l10n, @nextcloud/router
CnCardGrid@nextcloud/l10n, @nextcloud/vue, @nextcloud/auth, @nextcloud/event-bus

Only CnCard was clean all the way down — 12 of 13 unsafe. Publishing those under a name promising public safety would have shipped a guarantee that the first public deployment disproved.

@nextcloud/l10n is what almost all of them trip over. So these blocks take every string as a prop instead of calling t(). That one decision is what makes them portable, and it is the rule to follow when adding more.

The guarantee is checked, not asserted​

npm run check:public-safe

Walks the entry point's transitive imports and fails on any @nextcloud/*, naming the chain that reached it. It also fails when it inspects fewer than two files — "no violations found" and "nothing was read" must never be the same outcome.

Both failure modes are verified by running them, not by reading the code.

What is in the vocabulary​

keycomponentnotes
heroCnSiteHerofull-bleed band, optional search, optional background image
searchCnSiteSearchreal <form>, real <label>, emits its term
sectionCnSiteSectionfull-bleed band with a constrained .container
cardGridCnSiteCardGridauto-fit grid, reflows without media queries
cardCnSiteCardicon, description, named link
emptyStateCnSiteEmptyStateloading / empty / error, with the right ARIA for each
glossaryCnSiteGlossary<dl> of terms; renders synonyms, takes its rows as a prop
—CnSiteIconclosed icon vocabulary, inline SVG

siteBlockFor(key) returns null for anything unknown — a renderer needs that signal to show "unknown block" rather than silently omit content.

Conventions these blocks follow​

  • Strings are props. No t(), ever. See above. That includes the small connective words — CnSiteGlossary takes its "Ook bekend als:" and "Bron:" labels as props, because this library is not entitled to choose the wording, or the language, on behalf of a Dutch government portal.
  • Data is a prop too. A block never fetches. The host has already loaded the rows over its own contract; passing them down costs nothing and is what keeps the block renderable where there is no session to fetch with.
  • Heading levels are props, and the class tracks the level. The design system styles .utrecht-heading-2, not h2; a bare tag renders unstyled. A fixed level also skips outline levels wherever the block is placed.
  • Icons are names from a closed set, never path data. Page content is authored input, and raw SVG from an author is attacker-controlled markup inside an <svg> on a public government page.
  • Bands own their container. siteBlockIsBand(key) tells a host which blocks must NOT be wrapped in its content column — rendering a hero inside one clamps it (measured 1168px against a design of 1280).
  • No colours in component styles. A block lands on a surface it cannot know. Three separate defects in the consuming portal came from rules that coloured text without reference to its background.

Consumers​

portaliq​

ConductionNL/portaliq renders the fleet's public portals from this entry point. Its WidgetGrid composes its allow-list as { markdown: <its own>, ...siteBlockRegistry }.

Two things worth knowing if you change anything here:

  • markdown stays portaliq's. It renders untrusted authored content and its sanitisation posture is that app's decision, not a library default.
  • portaliq ships no design tokens. It deleted its own copy of the VNG set in favour of the nldesign app owning them, so a portal needs nldesign installed. Its e2e suite provisions it and asserts the token layer is present, because a page with no tokens renders perfectly and is simply unstyled — no error, no failed request.

Adding a block here is enough for portaliq to render it; adding a token is a change in nldesign, not this package.