Skip to main content

Asking Portaliq to create a landing page

An app on the same Nextcloud instance can ask Portaliq to provision a draft landing page with a bound lead-capture form, then receive each visitor's submission back. This is a same-instance cross-app command, not an HTTP call: both directions run as a typed Nextcloud event with a result slot (ADR-041). There is no new route to call and no bearer token to mint.

The page always lands in draft status. Publishing stays an editor's own action in the CMS designer.

Request a page

Dispatch OCA\Portaliq\Event\LandingPageRequestedEvent and read the result off the same event instance right after dispatch:

$event = new \OCA\Portaliq\Event\LandingPageRequestedEvent(
sourceApp: 'YOUR_APP_ID',
portal: 'open-tilburg',
route: '/campagne/YOUR_ROUTE_HERE',
title: 'Webinar: AI voor gemeenten',
locale: 'nl',
article: [
'summary' => 'Praktische AI-toepassingen voor de publieke sector.',
'body' => "## Programma\n\n- ...",
'heroImageRef' => null,
'links' => [],
],
form: [
'fields' => [
['id' => 'name', 'label' => 'Naam', 'type' => 'text', 'required' => true],
['id' => 'email', 'label' => 'E-mail', 'type' => 'email', 'required' => true],
],
'submitLabel' => 'Aanmelden',
'consentText' => 'Ik ga akkoord met de verwerking van mijn gegevens.',
],
utm: ['campaign' => 'webinar-ai-2026q4', 'source' => 'newsletter', 'medium' => 'email'],
externalReference: 'YOUR_APP_ID:campaign:00000000-0000-0000-0000-000000000000',
correlationId: 'YOUR_CORRELATION_ID_HERE',
);

$this->eventDispatcher->dispatchTyped($event);

if ($event->getError() !== null) {
// unknown_portal | duplicate_route | invalid_article | invalid_form | write_failed
}

$pageId = $event->getPageId();
$formId = $event->getFormId();
$publicUrl = $event->getPublicUrl(); // string or null, see below

getPublicUrl() returns null when the portal has no domain configured yet. route is checked for uniqueness within the portal before anything is written: a collision returns duplicate_route and creates nothing.

Guard the dispatch with class_exists() so your app stays installable without Portaliq:

if (class_exists(\OCA\Portaliq\Event\LandingPageRequestedEvent::class) === false) {
throw new \RuntimeException('Portaliq is not installed');
}

Receive a submission

When a visitor submits the form, Portaliq writes the submission and then looks for a class named OCA\{YourApp}\Event\LandingPageFormSubmittedEvent, where {YourApp} is the studly-cased form of the sourceApp you passed above (pipelinq becomes Pipelinq). Define that class in your own app, matching this constructor:

namespace OCA\YourApp\Event;

class LandingPageFormSubmittedEvent extends \OCP\EventDispatcher\Event {
public function __construct(
string $sourceApp,
string $formId,
string $pageId,
string $pageRoute,
string $portal,
string $externalReference,
array $values,
array $utmFirstTouch,
array $utmLastTouch,
string $referrer,
string $submittedAt,
string $nonce,
string $correlationId = '',
) {
// store what you need, register an IEventListener for this class
}
}

values carries only the fields you declared. utmFirstTouch and utmLastTouch are the campaign parameters captured client-side on the visitor's first and most recent landing (campaign, source, medium, term, content), each possibly null. submittedAt and nonce are generated by Portaliq and are never client-supplied.

If your app has not shipped this class yet, or is not installed, Portaliq logs the gap and moves on. The visitor's submission is already saved by the time this notification would fire, so a missing consumer never turns a successful submission into an error on the visitor's side. Nothing retries the notification, so a class that arrives later only picks up submissions made after it exists.

What Portaliq does not do here

Portaliq renders the page and collects the submission. It does not create a lead, book revenue, or run attribution logic: that stays in your app, built against the values and UTM data your LandingPageFormSubmittedEvent receives.

Next step

Register your app's LandingPageFormSubmittedEvent listener, then dispatch a LandingPageRequestedEvent from wherever your campaign is created.