Add per-form Listmonk list UUIDs to newsletter plugin

Add support for specifying Listmonk list UUIDs per form instance
Update petition form to use metadata-defined UUIDs
Add success confirmation message to newsletter forms
Update documentation with new functionality
This commit is contained in:
Ruben 2026-02-07 15:34:43 +01:00
parent 15a8f97cb2
commit 36591e7438
5 changed files with 47 additions and 17 deletions

View file

@ -2,6 +2,7 @@ date = "2026-01-15"
plugins = "petition-form"
thank_you_page = "takk"
hide_list = true
newsletter_list_uuids = "dfcf73f4-c86a-43a1-9ddb-31309f7392a9,c4849164-d5e7-4aca-9721-423282773fa1"
title = "Underskriftskampanje: Ja til medisinsk cannabis på resept!"
[en]

View file

@ -9,7 +9,7 @@
return [
'enabled' => true,
'url' => 'https://newsletter.example.com', // Your Listmonk URL (no trailing slash)
'list_uuids' => [
'list_uuids' => [ // Default lists (used when no per-form UUIDs are specified)
'UUID1',
'UUID2'
]

View file

@ -12,6 +12,7 @@
* Then in your PHP content file, call the function with your custom text and theme:
* <?= newsletter_signup('Your custom intro text here', 'hero') ?>
* <?= newsletter_signup('Short text here', 'small') ?>
* <?= newsletter_signup('Text', 'small', ['list-uuid-here']) ?>
*
* Available themes:
* - 'hero': Full-width gradient background section (like CTA sections)
@ -57,7 +58,7 @@ function newsletterT(string $key): string {
/**
* Subscribe to newsletter via Listmonk public API
*/
function newsletterSubscribe(string $email, string $name): array {
function newsletterSubscribe(string $email, string $name, array $listUuids = []): array {
$configPath = dirname(__DIR__, 2) . '/listmonk-config.php';
if (!file_exists($configPath)) {
return ['success' => false, 'error' => 'config_missing'];
@ -71,7 +72,7 @@ function newsletterSubscribe(string $email, string $name): array {
$payload = json_encode([
'email' => $email,
'name' => $name,
'list_uuids' => $config['list_uuids']
'list_uuids' => !empty($listUuids) ? $listUuids : $config['list_uuids']
]);
$url = $config['url'] . '/api/public/subscription';
@ -210,12 +211,15 @@ SCRIPT;
/**
* Render the "hero" theme - full-width gradient background section
*/
function newsletterRenderHero(string $introText, string $formId): string {
function newsletterRenderHero(string $introText, string $formId, array $listUuids = []): string {
$csrfToken = $_SESSION['newsletter_csrf_token'];
$nonce = bin2hex(random_bytes(8));
$escapedIntro = htmlspecialchars($introText, ENT_QUOTES, 'UTF-8');
$escapedFormId = htmlspecialchars($formId, ENT_QUOTES, 'UTF-8');
$t = newsletterGetTranslations();
$listUuidsField = !empty($listUuids)
? "\n" . ' <input type="hidden" name="newsletter_list_uuids" value="' . htmlspecialchars(implode(',', $listUuids), ENT_QUOTES, 'UTF-8') . '">'
: '';
$html = <<<HTML
<section class="newsletter-section newsletter-hero escape" id="{$escapedFormId}">
@ -223,7 +227,7 @@ function newsletterRenderHero(string $introText, string $formId): string {
<p class="newsletter-intro">{$escapedIntro}</p>
<form class="newsletter-form" method="post" data-newsletter-form data-success-message="{$t['successMessage']}" data-error-message="{$t['errorMessage']}">
<input type="hidden" name="newsletter_csrf" value="{$csrfToken}">
<input type="hidden" name="newsletter_form_id" value="{$escapedFormId}">
<input type="hidden" name="newsletter_form_id" value="{$escapedFormId}">{$listUuidsField}
<div class="newsletter-fields">
<div class="newsletter-field">
<label for="newsletter_name_{$nonce}" class="visually-hidden">{$t['nameLabel']}</label>
@ -252,19 +256,22 @@ HTML;
/**
* Render the "small" theme - compact inline notice with border
*/
function newsletterRenderSmall(string $introText, string $formId): string {
function newsletterRenderSmall(string $introText, string $formId, array $listUuids = []): string {
$csrfToken = $_SESSION['newsletter_csrf_token'];
$nonce = bin2hex(random_bytes(8));
$escapedIntro = htmlspecialchars($introText, ENT_QUOTES, 'UTF-8');
$escapedFormId = htmlspecialchars($formId, ENT_QUOTES, 'UTF-8');
$t = newsletterGetTranslations();
$listUuidsField = !empty($listUuids)
? "\n" . ' <input type="hidden" name="newsletter_list_uuids" value="' . htmlspecialchars(implode(',', $listUuids), ENT_QUOTES, 'UTF-8') . '">'
: '';
$html = <<<HTML
<aside class="newsletter-section newsletter-small" id="{$escapedFormId}">
<p class="newsletter-intro">{$escapedIntro}</p>
<form class="newsletter-form" method="post" data-newsletter-form data-success-message="{$t['successMessage']}" data-success-confirm="{$t['successConfirm']}" data-error-message="{$t['errorMessage']}">
<input type="hidden" name="newsletter_csrf" value="{$csrfToken}">
<input type="hidden" name="newsletter_form_id" value="{$escapedFormId}">
<input type="hidden" name="newsletter_form_id" value="{$escapedFormId}">{$listUuidsField}
<div class="newsletter-fields">
<div class="newsletter-field">
<label for="newsletter_email_{$nonce}" class="visually-hidden">{$t['emailLabel']}</label>
@ -464,19 +471,20 @@ STYLES;
*
* @param string $introText Custom intro text to display above the form
* @param string $theme Theme to use: 'hero' (default) or 'small'
* @param array $listUuids Optional Listmonk list UUIDs for this form (default: uses global config)
* @param string $formId Optional form ID for the section (default: 'newsletter')
* @return string The complete HTML for the newsletter signup section
*/
function newsletter_signup(string $introText, string $theme = 'hero', string $formId = 'newsletter'): string {
function newsletter_signup(string $introText, string $theme = 'hero', array $listUuids = [], string $formId = 'newsletter'): string {
$html = newsletterGetStyles();
switch ($theme) {
case 'small':
$html .= newsletterRenderSmall($introText, $formId);
$html .= newsletterRenderSmall($introText, $formId, $listUuids);
break;
case 'hero':
default:
$html .= newsletterRenderHero($introText, $formId);
$html .= newsletterRenderHero($introText, $formId, $listUuids);
break;
}
@ -522,8 +530,20 @@ function newsletterHandleSubmission(): void {
exit;
}
// Parse per-form list UUIDs (if provided)
$listUuids = [];
if (!empty($_POST['newsletter_list_uuids'])) {
$raw = explode(',', $_POST['newsletter_list_uuids']);
foreach ($raw as $uuid) {
$uuid = trim($uuid);
if (preg_match('/^[0-9a-f\-]{36}$/i', $uuid)) {
$listUuids[] = $uuid;
}
}
}
// Subscribe
$result = newsletterSubscribe($email, $name);
$result = newsletterSubscribe($email, $name, $listUuids);
if ($result['success']) {
$_SESSION['newsletter_last_submit'] = time();

View file

@ -46,7 +46,7 @@ function petitionSanitizeCSV(string $value): string {
* Subscribe email to Listmonk newsletter lists via public API
* Listmonk handles double opt-in (sends its own confirmation email)
*/
function petitionSubscribeToNewsletter(string $email, string $name): bool {
function petitionSubscribeToNewsletter(string $email, string $name, array $listUuids = []): bool {
$configPath = dirname(__DIR__, 2) . '/listmonk-config.php';
if (!file_exists($configPath)) {
error_log("Listmonk config not found: {$configPath}");
@ -59,13 +59,15 @@ function petitionSubscribeToNewsletter(string $email, string $name): bool {
return false;
}
$uuids = !empty($listUuids) ? $listUuids : $config['list_uuids'];
// Log the UUIDs being used
error_log("Listmonk attempting subscription with UUIDs: " . implode(', ', $config['list_uuids']));
error_log("Listmonk attempting subscription with UUIDs: " . implode(', ', $uuids));
$payload = json_encode([
'email' => $email,
'name' => $name,
'list_uuids' => $config['list_uuids']
'list_uuids' => $uuids
]);
$url = $config['url'] . '/api/public/subscription';
@ -1238,6 +1240,9 @@ function petitionGetPageData(?Context $ctx): ?array {
// Use petition_title if set (for subpages), otherwise fall back to page title
$petitionTitle = $metadata['petition_title'] ?? $metadata['title'] ?? $petitionId;
$thankYouPage = $metadata['thank_you_page'] ?? 'takk';
$newsletterListUuids = !empty($metadata['newsletter_list_uuids'])
? array_map('trim', explode(',', $metadata['newsletter_list_uuids']))
: [];
$formErrors = [];
$formData = ['firstname' => '', 'surname' => '', 'email' => '', 'region' => '', 'display' => 'semi'];
@ -1472,7 +1477,7 @@ function petitionGetPageData(?Context $ctx): ?array {
error_log("Newsletter checkbox: " . ($newsletterOptIn ? 'checked' : 'not checked'));
if ($newsletterOptIn) {
$fullName = $formData['firstname'] . ' ' . $formData['surname'];
petitionSubscribeToNewsletter($formData['email'], $fullName);
petitionSubscribeToNewsletter($formData['email'], $fullName, $newsletterListUuids);
}
$_SESSION['last_petition_submit'] = time();

View file

@ -17,6 +17,7 @@ Then call from PHP content files:
```php
<?= newsletter_signup('Your custom intro text here', 'hero') ?>
<?= newsletter_signup('Short text here', 'small') ?>
<?= newsletter_signup('Text', 'small', ['list-uuid-1', 'list-uuid-2']) ?>
```
## Themes
@ -29,11 +30,12 @@ Then call from PHP content files:
## Function Signature
```php
newsletter_signup(string $introText, string $theme = 'hero', string $formId = 'newsletter'): string
newsletter_signup(string $introText, string $theme = 'hero', array $listUuids = [], string $formId = 'newsletter'): string
```
- `$introText` - Displayed above the form
- `$theme` - `'hero'` or `'small'`
- `$listUuids` - Listmonk list UUIDs for this form (empty = use global config)
- `$formId` - HTML id for the section (for anchor links)
## How It Works
@ -58,6 +60,8 @@ return [
Uses Listmonk's **public** subscription API (`/api/public/subscription`). No authentication needed. Listmonk handles its own double opt-in flow.
Per-form list UUIDs can be passed via `$listUuids` parameter. When empty, falls back to the global config's `list_uuids`.
## Anti-Spam
1. **CSRF token** - Separate from petition (`newsletter_csrf_token`)
@ -71,7 +75,7 @@ All use `newsletter.*` prefix in language files:
- `newsletter.email_label`, `newsletter.email_placeholder`
- `newsletter.notice` (e.g., "We send about 12 emails per year")
- `newsletter.submit_button`
- `newsletter.success_message`, `newsletter.error_message`
- `newsletter.success_message`, `newsletter.success_confirm`, `newsletter.error_message`
## Static Resources