Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
958ab14
feat(news): staff can put a news item on one portal's public website,…
rubenvdlinde Oct 5, 2026
423627c
feat(site): task tiles, public news, a dated list and the sign-in car…
rubenvdlinde Oct 5, 2026
808456a
fix(news): phpcs and phpmd on the public news reader and the staff ne…
rubenvdlinde Oct 5, 2026
735f62a
fix(site): the news widgets' mounted hooks name their requirement
rubenvdlinde Oct 5, 2026
89f6c57
wip(site-chrome): portal schema and content API serve the header sear…
rubenvdlinde Oct 5, 2026
e2eec02
chore: untrack two local dependency links committed by mistake
rubenvdlinde Oct 5, 2026
91198b8
fix(site): the sign-in card's button keeps its own colours whatever t…
rubenvdlinde Oct 5, 2026
6135a95
fix(site): the inverse sign-in card's button is a plain button, so no…
rubenvdlinde Oct 5, 2026
7e33da5
wip(site-chrome): designed header (search, account button, person chi…
rubenvdlinde Oct 5, 2026
5ab4b20
Merge development into feat/site-school-blocks-home
rubenvdlinde Oct 5, 2026
7e89d90
wip(site-chrome): node tests for the header tools, footer, sign-in ca…
rubenvdlinde Oct 5, 2026
71da6f8
wip(site-chrome): one signed-out block, the person helper in the lazy…
rubenvdlinde Oct 5, 2026
3a42b1c
wip(site-chrome): designed footer columns, fonts, openspec change
rubenvdlinde Oct 5, 2026
a80f8e6
fix(site-chrome): no inline ifs in the shell projection
rubenvdlinde Oct 5, 2026
727b7c9
feat(site-chrome): a declared header search shows without enabled, wi…
rubenvdlinde Oct 5, 2026
663dfcf
fix(site-chrome): the brand column keeps the design's spacing
rubenvdlinde Oct 5, 2026
cce40cc
test(site-chrome): import order
rubenvdlinde Oct 5, 2026
e4c154a
chore: merge feat/site-chrome-follows-the-design, the header, footer …
rubenvdlinde Oct 5, 2026
6450631
refactor(site-chrome): the sign-in text in its own class, logo varian…
rubenvdlinde Oct 5, 2026
52b99cf
chore: merge feat/site-school-blocks-home, the home page blocks this …
rubenvdlinde Oct 5, 2026
1ce1f1f
fix(site-chrome): on a phone the motif runs under the masthead while …
rubenvdlinde Oct 5, 2026
f35170e
fix(site-chrome): controller test knows the logo variants; docblock l…
rubenvdlinde Oct 5, 2026
8f832d8
fix(site-chrome): the sign-in page fits a phone
rubenvdlinde Oct 5, 2026
524b939
fix(site-chrome): the breadcrumb starts under the motif
rubenvdlinde Oct 5, 2026
2e738e2
wip(example-site): the Zuiddrecht declaration, the installer with its…
rubenvdlinde Oct 5, 2026
009d417
wip(example-site): unit tests for the installer, store and commands; …
rubenvdlinde Oct 5, 2026
334c848
wip(example-site): the current menu item may sit in the line; the dec…
rubenvdlinde Oct 5, 2026
f7cafde
docs(site-chrome): live check done
rubenvdlinde Oct 5, 2026
62c0c75
wip(example-site): the hero heading shows; the site names a set's gre…
rubenvdlinde Oct 5, 2026
2ec6603
chore: merge development (home blocks #1195 and Mijn displays #1198 l…
rubenvdlinde Oct 5, 2026
c3faf49
chore: merge feat/site-chrome-follows-the-design at f7cafde6 (#1202);…
rubenvdlinde Oct 5, 2026
61ef996
wip(example-site): the designed site reads in the set's family, table…
rubenvdlinde Oct 5, 2026
6f29640
style(example-site): prettier
rubenvdlinde Oct 5, 2026
e0562fe
refactor(example-site): the installer, the remover, the record, the r…
rubenvdlinde Oct 5, 2026
73207e9
chore: merge development (#1200, #1205)
rubenvdlinde Oct 5, 2026
defafc7
chore: merge development (#1202 and #1203 landed)
rubenvdlinde Oct 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@
<!-- portal-page-traffic: rebuild the retained days now, the same
back-fill the aggregation job runs once after an upgrade. -->
<command>OCA\Portaliq\Command\TrafficReaggregate</command>
<!-- example-site-zuiddrecht: put a shipped example site on the
instance, and take it off again. -->
<command>OCA\Portaliq\Command\ExampleSiteInstall</command>
<command>OCA\Portaliq\Command\ExampleSiteRemove</command>
</commands>

<settings>
Expand Down
99 changes: 99 additions & 0 deletions css/site-theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,105 @@
background-color: var(--thematiq-accent-color, var(--nldesign-color-primary));
}

/*
* A TABLE AND A LIST OF LINKS ARE READABLE WITHOUT A ROLE FOR THEM, TOO
* (example-site-zuiddrecht).
*
* Like the button above: the Utrecht table and link list read every measure
* from a token and declare no fallback. The theme app's bridge names the
* table's inline padding and nothing else of the two, so on a set that only
* declares the `--nldesign-*` layer a table had no room above or under a
* cell and no line between its rows, and the links of a list stood line on
* line. Measured on the Zuiddrecht content page: cell padding-top 0px, row
* border 0px, list row-gap `normal`.
*
* Each declaration reads the set's own token first, so a set that names the
* role renders as before. The line between rows is the set's border colour,
* else the text's own.
*/
.pq-site .utrecht-table__cell,
.pq-site .utrecht-table__header-cell {
padding-block: var(--utrecht-table-cell-padding-block-start, 12px)
var(--utrecht-table-cell-padding-block-end, 12px);
border-block-end: var(--utrecht-table-row-border-block-end-width, 1px) solid
var(
--utrecht-table-row-border-block-end-color,
var(
--nldesign-color-border-dark,
var(--nldesign-color-border, currentcolor)
)
);
text-align: start;
}

.pq-site .utrecht-table__caption {
margin-block-end: var(--utrecht-table-caption-margin-block-end, 8px);
text-align: var(--utrecht-table-caption-text-align, start);
}

.pq-site .utrecht-link-list {
row-gap: var(--utrecht-link-list-row-gap, 10px);
}

.pq-site .utrecht-link-list__description {
display: block;
}

/*
* THE DESIGNED SITE READS IN THE SET'S OWN FAMILY (example-site-zuiddrecht).
*
* The vendored sheet sets the page's family on `body` as a literal, so every
* text that inherits it (the menu, the hero heading, task labels, link lists,
* news titles) stayed in that face while paragraphs and buttons, which read a
* token, took the set's. Measured on the Zuiddrecht home page: 40 of 78 text
* runs in the vendored face under a set that names Fira Sans.
*
* Only for a portal with the designed header: every other portal keeps the
* face it has, which its tests measure on the menu.
*/
.pq-site:has(.pq-site__header--designed),
body:has(.pq-site__header--designed) .pq-site__skip {
font-family: var(--utrecht-document-font-family, inherit);
}

/*
* And its last block does not stand against the footer. A band (a hero, a
* coloured section) brings its own room; a page that ends in text or links
* has none, and the footer's stripe then touched the last line.
*/
.pq-site:has(.pq-site__header--designed) .pq-site__main {
padding-block-end: 64px;
}

/*
* THE CURRENT ITEM IN THE LINE (example-site-zuiddrecht). A set whose header
* carries a plain line under the menu may mark the page on screen as a piece
* of that line, in a colour of its own: Zuiddrecht's line is red and the
* piece under the current item is blue. The set says so with two tokens:
*
* --nldesign-website-nav-current-in-line 1 moves the bar down into the
* line and makes it as high as it
* --nldesign-website-nav-current-color the bar's colour
*
* Without them nothing changes: no shift, four pixels, the accent. The bar
* sits above the line (z-index 2 over the line's 1), so it covers its piece.
*/
.pq-site .ac-header .ac-c-navigation__link-container[aria-current='page']::after {
--pq-nav-current-in-line: var(--nldesign-website-nav-current-in-line, 0);
inset-block-end: calc(
var(--pq-nav-current-in-line) * -1 * var(--cn-brand-stripe-height, 0px)
);
z-index: 2;
block-size: calc(
4px + var(--pq-nav-current-in-line) *
(var(--cn-brand-stripe-height, 4px) - 4px)
);
background-color: var(
--nldesign-website-nav-current-color,
var(--thematiq-accent-color, var(--nldesign-color-primary))
);
}

.pq-site .ac-header .ac-c-navigation__li {
position: relative;
}
Expand Down
87 changes: 87 additions & 0 deletions docs/Installation/example-site-zuiddrecht.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
title: Install the Zuiddrecht example site
sidebar_label: Zuiddrecht example site
description: Put the website of the demo municipality Zuiddrecht on your instance with one command, and take it off again
---

# Install the Zuiddrecht example site

Zuiddrecht is a municipality that does not exist. Its website shows what a portal looks like when it is finished: a header with search and a button to the resident's own area, a home page with top tasks and news, content pages and a footer. You install it with one command and remove it with one command.

## What you need

- Portaliq and OpenRegister, both enabled.
- The theme app (thematiq) with the `zuiddrecht` set. Without it the site installs and shows without its house style. The command tells you when the set is missing.

## Install

```bash
occ portaliq:example-site:install zuiddrecht
```

You get one portal, three menus, 33 pages and four news items:

```
Portal: 1 declared, 1 created, 0 already there, 1 found afterwards
Menus: 3 declared, 3 created, 0 already there, 3 found afterwards
Pages: 33 declared, 33 created, 0 already there, 33 found afterwards
News items: 4 declared, 4 created, 0 already there, 4 found afterwards
The site is installed. Open it at /index.php/apps/portaliq/site?portal=zuiddrecht
```

Open that address in a browser. You do not have to sign in.

The command only writes what is missing. A portal with the slug `zuiddrecht`, a menu with the same position and title, a page at the same route and a news item with the same title stay exactly as they are. So you can run it again after an editor changed a page: the change stays.

## Read the result

After writing, the command reads your instance back and compares it with what it meant to write. The last number on each line is what it found.

| Exit code | Meaning |
| --- | --- |
| 0 | Everything is on the instance. |
| 1 | Nothing was written: the site name is unknown, or OpenRegister is not available. |
| 2 | Something is missing. Each missing object and each lost key is named. |

A line such as `Written but not kept: portal zuiddrecht: headerSearch.label` means the portaliq register on your instance is older than the app. Run `occ maintenance:repair` and install again.

## Open the site on your own address

The site answers at `/index.php/apps/portaliq/site?portal=zuiddrecht`. To serve it on a hostname of its own, add the hostname to the portal and verify it. [Custom domains](../operations/custom-domains.md) explains how. The install never claims a hostname for you.

## Signing in

The header shows the button "Mijn Zuiddrecht". The portal names DigiD and eHerkenning as its ways in, as the design does. They work once your instance has a sign-in broker for them: see [Signing in through integriq](../operations/signing-in-through-integriq.md). On a test instance you can switch on the test sign-in instead:

```bash
occ config:app:set portaliq dev_login_enabled --value=yes
```

Never switch that on for a site real residents use.

## What is and is not in the site

- The header, footer, home page and the page "Afval scheiden en ophalen" follow the Zuiddrecht design. The other pages are short, so that no link is dead.
- "Woo-publicaties" and the search box open `/zoeken`, with the search block every portal can place. It finds publications when OpenCatalogi is installed.
- Phone numbers and addresses read `[telefoonnummer]`, `[e-mailadres]` and `[adres]`. Zuiddrecht has none.

## Remove

```bash
occ portaliq:example-site:remove zuiddrecht
```

This deletes the objects the install created, from the install's own record, and nothing else:

```
Menus: 3 deleted, 0 already gone
Pages: 33 deleted, 0 already gone
News items: 4 deleted, 0 already gone
Portal: deleted
```

The portal stays when it was there before the install, or when it still holds a menu or a page you added yourself. The command then says so and ends with exit code 2. Delete or move your own content and run it again.

## Next

Change the site into your own: open **Portaliq → Portals**, pick `zuiddrecht` and edit its pages. To give it another house style, see [Choosing a portal's house style](../operations/choosing-a-portal-house-style.md).
145 changes: 145 additions & 0 deletions lib/Command/ExampleSiteInstall.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

/**
* Portaliq Example Site Install Command
*
* `occ portaliq:example-site:install <site>`: put a shipped example site on
* this instance. Writes what is missing, never changes what is there, and
* reads the instance back to say what arrived.
*
* @category Command
* @package OCA\Portaliq\Command
*
* @author Conduction Development Team <info@conduction.nl>
* @copyright 2026 Conduction B.V.
* @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
* SPDX-License-Identifier: EUPL-1.2
*
* @link https://conduction.nl
*
* @spec openspec/changes/example-site-zuiddrecht/specs/example-site/spec.md#requirement-an-administrator-must-be-able-to-install-an-example-site-with-one-command
*/

declare(strict_types=1);

namespace OCA\Portaliq\Command;

use OCA\Portaliq\Service\ExampleSite\ExampleSiteCatalogue;
use OCA\Portaliq\Service\ExampleSite\ExampleSiteInstaller;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

/**
* Install an example site.
*
* Exit codes: 0 everything declared is on the instance, 1 nothing could be
* done (unknown site, OpenRegister missing), 2 something did not arrive.
*
* @spec openspec/changes/example-site-zuiddrecht/specs/example-site/spec.md#requirement-an-administrator-must-be-able-to-install-an-example-site-with-one-command
*/
class ExampleSiteInstall extends Command {
/**
* The word the output uses for each schema.
*/
private const WORDS = ['portal' => 'Portal', 'menu' => 'Menus', 'page' => 'Pages', 'newsItem' => 'News items'];

/**
* Constructor.
*
* @param ExampleSiteCatalogue $catalogue The shipped sites.
* @param ExampleSiteInstaller $installer Writes and proves.
*
* @return void
*/
public function __construct(
private readonly ExampleSiteCatalogue $catalogue,
private readonly ExampleSiteInstaller $installer,
) {
parent::__construct();
}//end __construct()

/**
* Name and describe the command.
*
* @return void
*
* @spec openspec/changes/example-site-zuiddrecht/specs/example-site/spec.md#requirement-an-administrator-must-be-able-to-install-an-example-site-with-one-command
*/
protected function configure(): void {
$this->setName(name: 'portaliq:example-site:install');
$this->setDescription(description: 'Install a shipped example site: its portal, menus, pages and news. Existing content is never changed');
$this->addArgument(name: 'site', mode: InputArgument::REQUIRED, description: 'The example site, for example zuiddrecht');
}//end configure()

/**
* Install the site and report what arrived.
*
* @param InputInterface $input The command input.
* @param OutputInterface $output The command output.
*
* @return int 0, 1 or 2; see the class.
*
* @spec openspec/changes/example-site-zuiddrecht/specs/example-site/spec.md#requirement-the-install-must-prove-what-arrived
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
$id = trim((string)$input->getArgument('site'));
$site = $this->catalogue->find(id: $id);
if ($site === null) {
$output->writeln('<error>No example site "' . $id . '". Sites you can install: ' . implode(', ', $this->catalogue->ids()) . '</error>');

return 1;
}

$report = $this->installer->install(site: $site);
if ($report['available'] === false) {
$output->writeln('<error>OpenRegister is not available, so nothing was written. Enable openregister and run this again.</error>');

return 1;
}

foreach ($report['types'] as $schema => $counts) {
$output->writeln(
sprintf(
'%s: %d declared, %d created, %d already there, %d found afterwards',
self::WORDS[$schema],
$counts['declared'],
$counts['created'],
$counts['kept'],
$counts['arrived']
)
);
}

if ($report['themeOffered'] === false) {
$output->writeln(
'<comment>The theme app does not offer the set "' . (string)($site['portal']['theme'] ?? '')
. '". The site shows without its house style until the theme app (thematiq) with that set is installed.</comment>'
);
}

foreach ($report['missing'] as $missing) {
$output->writeln('<error>Not on the instance: ' . $missing . '</error>');
}

foreach ($report['lost'] as $lost) {
$output->writeln('<error>Written but not kept: ' . $lost . '</error>');
}

if ($report['ok'] === false) {
$output->writeln(
'<error>The site is not complete. A key that is not kept usually means the portaliq register is older than this app:'
. ' run "occ maintenance:repair" and install again.</error>'
);

return 2;
}

$output->writeln('The site is installed. Open it at /index.php/apps/portaliq/site?portal=' . $report['portal']);

return 0;
}//end execute()
}//end class
Loading
Loading