From 97cec66083890b2f961e8dafa45a2ecd50f13639 Mon Sep 17 00:00:00 2001 From: Marijn Doeve Date: Sun, 12 Jul 2026 22:57:16 +0200 Subject: [PATCH] Add Inky-based, WIDM-styled email templates (#212) Adds twig/inky-extra and twig/cssinliner-extra to build the password reset and email confirmation emails from a shared responsive layout, styled after the quiz's green/black look, with Dutch copy in the show's dramatic tone. --- .idea/TijdVoorDeTest.iml | 11 +- .idea/php.xml | 11 + Dockerfile | 1 + composer.json | 2 + composer.lock | 387 ++++++++++++++---- config/reference.php | 4 +- .../registration/confirmation_email.html.twig | 29 +- templates/emails/layout.html.twig | 168 ++++++++ templates/reset_password/email.html.twig | 28 +- 9 files changed, 554 insertions(+), 87 deletions(-) create mode 100644 templates/emails/layout.html.twig diff --git a/.idea/TijdVoorDeTest.iml b/.idea/TijdVoorDeTest.iml index 01637f5..7f9645d 100644 --- a/.idea/TijdVoorDeTest.iml +++ b/.idea/TijdVoorDeTest.iml @@ -4,7 +4,6 @@ - @@ -172,6 +171,16 @@ + + + + + + + + + + diff --git a/.idea/php.xml b/.idea/php.xml index 1019d6e..3df09df 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -206,6 +206,16 @@ + + + + + + + + + + @@ -403,6 +413,7 @@ + diff --git a/Dockerfile b/Dockerfile index 45228a4..463b1ae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,6 +32,7 @@ RUN set -eux; \ opcache \ zip \ gd \ + xsl \ ; # https://getcomposer.org/doc/03-cli.md#composer-allow-superuser diff --git a/composer.json b/composer.json index 7263fe8..9c420bf 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,9 @@ "symfonycasts/sass-bundle": "^0.10", "symfonycasts/verify-email-bundle": "^1.18.0", "thecodingmachine/safe": "^3.4.0", + "twig/cssinliner-extra": "^3.26.0", "twig/extra-bundle": "^3.24.0", + "twig/inky-extra": "^3.26.0", "twig/intl-extra": "^3.24.0", "twig/markdown-extra": "^3.24.0", "twig/twig": "^3.27.1" diff --git a/composer.lock b/composer.lock index acf6e95..e8b0db9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b2f319bad9d390a60067420d772e06e6", + "content-hash": "0bb88a3f492fa607203eb66735827179", "packages": [ { "name": "composer/pcre", @@ -1916,6 +1916,59 @@ ], "time": "2022-12-11T20:36:23+00:00" }, + { + "name": "lorenzo/pinky", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/lorenzo/pinky.git", + "reference": "e1b1bdb2c132b8a7ba32bca64d2443f646ddbd17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lorenzo/pinky/zipball/e1b1bdb2c132b8a7ba32bca64d2443f646ddbd17", + "reference": "e1b1bdb2c132b8a7ba32bca64d2443f646ddbd17", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xsl": "*", + "php": ">=5.6.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.21 || ^9.5.10" + }, + "type": "library", + "autoload": { + "files": [ + "src/pinky.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jose Lorenzo Rodriguez", + "email": "jose.zap@gmail.com" + } + ], + "description": "A Foundation for Emails (Inky) template transpiler", + "keywords": [ + "email", + "foundation", + "inky", + "template", + "zurb" + ], + "support": { + "issues": "https://github.com/lorenzo/pinky/issues", + "source": "https://github.com/lorenzo/pinky/tree/1.1.0" + }, + "time": "2023-07-31T13:36:50+00:00" + }, { "name": "maennchen/zipstream-php", "version": "3.2.2", @@ -4162,6 +4215,75 @@ ], "time": "2026-06-16T12:55:20+00:00" }, + { + "name": "symfony/css-selector", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "dc0e2be45c9b5588c82414f02ac574b4b986abcd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/dc0e2be45c9b5588c82414f02ac574b4b986abcd", + "reference": "dc0e2be45c9b5588c82414f02ac574b4b986abcd", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, { "name": "symfony/dependency-injection", "version": "v8.1.1", @@ -9051,6 +9173,130 @@ ], "time": "2026-02-04T18:08:13+00:00" }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" + }, + "time": "2025-12-02T11:56:42+00:00" + }, + { + "name": "twig/cssinliner-extra", + "version": "v3.26.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/cssinliner-extra.git", + "reference": "1b0dc906bbad7226c967bd325e99cccb1a850c4b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/cssinliner-extra/zipball/1b0dc906bbad7226c967bd325e99cccb1a850c4b", + "reference": "1b0dc906bbad7226c967bd325e99cccb1a850c4b", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "tijsverkoyen/css-to-inline-styles": "^2.0", + "twig/twig": "^3.13|^4.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Twig\\Extra\\CssInliner\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + } + ], + "description": "A Twig extension to allow inlining CSS", + "homepage": "https://twig.symfony.com", + "keywords": [ + "css", + "inlining", + "twig" + ], + "support": { + "source": "https://github.com/twigphp/cssinliner-extra/tree/v3.26.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2026-05-15T13:14:14+00:00" + }, { "name": "twig/extra-bundle", "version": "v3.24.0", @@ -9125,6 +9371,76 @@ ], "time": "2026-02-07T08:07:38+00:00" }, + { + "name": "twig/inky-extra", + "version": "v3.26.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/inky-extra.git", + "reference": "0a8b24f0d0247bf6dc5e2af0c0ab09c0c4e5343e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/inky-extra/zipball/0a8b24f0d0247bf6dc5e2af0c0ab09c0c4e5343e", + "reference": "0a8b24f0d0247bf6dc5e2af0c0ab09c0c4e5343e", + "shasum": "" + }, + "require": { + "lorenzo/pinky": "^1.0.5", + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "twig/twig": "^3.13|^4.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Twig\\Extra\\Inky\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + } + ], + "description": "A Twig extension for the inky email templating engine", + "homepage": "https://twig.symfony.com", + "keywords": [ + "email", + "emails", + "inky", + "twig" + ], + "support": { + "source": "https://github.com/twigphp/inky-extra/tree/v3.26.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2026-05-15T13:14:14+00:00" + }, { "name": "twig/intl-extra", "version": "v3.26.0", @@ -12909,75 +13225,6 @@ ], "time": "2026-06-09T10:54:51+00:00" }, - { - "name": "symfony/css-selector", - "version": "v8.1.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/css-selector.git", - "reference": "dc0e2be45c9b5588c82414f02ac574b4b986abcd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/dc0e2be45c9b5588c82414f02ac574b4b986abcd", - "reference": "dc0e2be45c9b5588c82414f02ac574b4b986abcd", - "shasum": "" - }, - "require": { - "php": ">=8.4.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\CssSelector\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Jean-François Simon", - "email": "jeanfrancois.simon@sensiolabs.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Converts CSS selectors to XPath expressions", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/css-selector/tree/v8.1.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-05-29T05:06:50+00:00" - }, { "name": "symfony/dom-crawler", "version": "v8.1.1", diff --git a/config/reference.php b/config/reference.php index 31b811b..a9a791e 100644 --- a/config/reference.php +++ b/config/reference.php @@ -1277,10 +1277,10 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * enabled?: bool|Param, // Default: true * }, * cssinliner?: bool|array{ - * enabled?: bool|Param, // Default: false + * enabled?: bool|Param, // Default: true * }, * inky?: bool|array{ - * enabled?: bool|Param, // Default: false + * enabled?: bool|Param, // Default: true * }, * string?: bool|array{ * enabled?: bool|Param, // Default: false diff --git a/templates/backoffice/registration/confirmation_email.html.twig b/templates/backoffice/registration/confirmation_email.html.twig index 7c79d8a..357b6d1 100644 --- a/templates/backoffice/registration/confirmation_email.html.twig +++ b/templates/backoffice/registration/confirmation_email.html.twig @@ -1,11 +1,22 @@ -

Hi! Please confirm your email!

+{% extends 'emails/layout.html.twig' %} -

- Please confirm your email address by clicking the following link:

- Confirm my Email. - This link will expire in {{ expiresAtMessageKey|trans(expiresAtMessageData, 'VerifyEmailBundle') }}. -

+{% block preheader %}Bevestig je e-mailadres voor Tijd voor de test.{% endblock %} -

- Cheers! -

+{% block heading %}Nog één stap voor je mee mag doen{% endblock %} + +{% block body %} +

Beste speler,

+

+ Welkom bij Tijd voor de test! Voor je echt aan de slag kunt, moeten we nog één ding + weten: of jij écht bent wie je zegt dat je bent. Klik daarom op de knop hieronder. +

+ +
+ +

+ Deze link verloopt over {{ expiresAtMessageKey|trans(expiresAtMessageData, 'VerifyEmailBundle') }}, dus + wacht niet te lang. +

+ +

Veel plezier!

+{% endblock %} diff --git a/templates/emails/layout.html.twig b/templates/emails/layout.html.twig new file mode 100644 index 0000000..13a4846 --- /dev/null +++ b/templates/emails/layout.html.twig @@ -0,0 +1,168 @@ +{% apply inline_css %} +{% apply inky_to_html %} + + + + + + {% block title %}Tijd voor de test{% endblock %} + + + +{% block preheader %}{% endblock %} + + + + + + + +{% endapply %} +{% endapply %} diff --git a/templates/reset_password/email.html.twig b/templates/reset_password/email.html.twig index c714d22..e62d49e 100644 --- a/templates/reset_password/email.html.twig +++ b/templates/reset_password/email.html.twig @@ -1,9 +1,27 @@ -

Hi!

+{% extends 'emails/layout.html.twig' %} -

To reset your password, please visit the following link

+{% block preheader %}Je hebt om een nieuw wachtwoord gevraagd voor Tijd voor de test.{% endblock %} -{{ url('tvdt_reset_password', {token: resetToken.token}) }} +{% block heading %}Tijd voor een nieuw wachtwoord{% endblock %} -

This link will expire in {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}.

+{% block body %} +

Beste speler,

+

+ Je hebt aangegeven dat je wachtwoord opnieuw moet worden ingesteld. Voordat je verder mag, is er nog één + opdracht: klik op de knop hieronder. +

-

Cheers!

+
+ +

+ Werkt de knop niet? Kopieer dan deze link naar je browser:
+ {{ url('tvdt_reset_password', {token: resetToken.token}) }} +

+ +

+ Let op: deze link is, niet eeuwig geldig. Hij verloopt over + {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}. +

+ +

Veel succes!

+{% endblock %}