Add CTAs to daily emails

Add a daily email CTA node type and reference field to attach a CTA to a
daily email.

Also update the RSS feed view to include the referenced body field
value.
This commit is contained in:
Oliver Davies 2025-06-13 11:14:11 +01:00
parent 76868079ac
commit 2d55ea78f1
24 changed files with 1058 additions and 3 deletions

View file

@ -0,0 +1,22 @@
uuid: 125295f6-32ba-473d-a114-64b2f30a74a2
langcode: en
status: true
dependencies:
config:
- node.type.daily_email_cta
id: node.daily_email_cta.promote
field_name: promote
entity_type: node
bundle: daily_email_cta
label: 'Promoted to front page'
description: ''
required: false
translatable: true
default_value:
-
value: 0
default_value_callback: ''
settings:
on_label: 'On'
off_label: 'Off'
field_type: boolean

View file

@ -4,6 +4,7 @@ status: true
dependencies:
config:
- field.field.node.daily_email.body
- field.field.node.daily_email.field_daily_email_cta
- node.type.daily_email
module:
- path
@ -29,6 +30,16 @@ content:
region: content
settings: { }
third_party_settings: { }
field_daily_email_cta:
type: entity_reference_autocomplete
weight: 121
region: content
settings:
match_operator: CONTAINS
match_limit: 10
size: 60
placeholder: ''
third_party_settings: { }
path:
type: path
weight: 30

View file

@ -0,0 +1,87 @@
uuid: 66add161-1d42-41b4-9eb3-adf47de3b200
langcode: en
status: true
dependencies:
config:
- field.field.node.daily_email_cta.body
- node.type.daily_email_cta
module:
- path
- text
id: node.daily_email_cta.default
targetEntityType: node
bundle: daily_email_cta
mode: default
content:
body:
type: text_textarea_with_summary
weight: 2
region: content
settings:
rows: 9
summary_rows: 3
placeholder: ''
show_summary: false
third_party_settings: { }
created:
type: datetime_timestamp
weight: 10
region: content
settings: { }
third_party_settings: { }
path:
type: path
weight: 30
region: content
settings: { }
third_party_settings: { }
promote:
type: boolean_checkbox
weight: 15
region: content
settings:
display_label: true
third_party_settings: { }
simple_sitemap:
weight: 10
region: content
settings: { }
third_party_settings: { }
status:
type: boolean_checkbox
weight: 120
region: content
settings:
display_label: true
third_party_settings: { }
sticky:
type: boolean_checkbox
weight: 16
region: content
settings:
display_label: true
third_party_settings: { }
title:
type: string_textfield
weight: -5
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
uid:
type: entity_reference_autocomplete
weight: 5
region: content
settings:
match_operator: CONTAINS
match_limit: 10
size: 60
placeholder: ''
third_party_settings: { }
url_redirects:
weight: 50
region: content
settings: { }
third_party_settings: { }
hidden: { }

View file

@ -4,6 +4,7 @@ status: true
dependencies:
config:
- field.field.node.daily_email.body
- field.field.node.daily_email.field_daily_email_cta
- node.type.daily_email
module:
- text
@ -21,4 +22,5 @@ content:
weight: 0
region: content
hidden:
field_daily_email_cta: true
links: true

View file

@ -5,6 +5,7 @@ dependencies:
config:
- core.entity_view_mode.node.rss
- field.field.node.daily_email.body
- field.field.node.daily_email.field_daily_email_cta
- node.type.daily_email
module:
- layout_builder
@ -27,4 +28,5 @@ content:
weight: 0
region: content
hidden:
field_daily_email_cta: true
links: true

View file

@ -5,6 +5,7 @@ dependencies:
config:
- core.entity_view_mode.node.teaser
- field.field.node.daily_email.body
- field.field.node.daily_email.field_daily_email_cta
- node.type.daily_email
module:
- text
@ -23,4 +24,5 @@ content:
weight: 0
region: content
hidden:
field_daily_email_cta: true
links: true

View file

@ -0,0 +1,28 @@
uuid: a09e31bc-1142-4441-a82c-aa658d1e45fa
langcode: en
status: true
dependencies:
config:
- field.field.node.daily_email_cta.body
- node.type.daily_email_cta
module:
- text
- user
id: node.daily_email_cta.default
targetEntityType: node
bundle: daily_email_cta
mode: default
content:
body:
type: text_default
label: hidden
settings: { }
third_party_settings: { }
weight: 0
region: content
links:
settings: { }
third_party_settings: { }
weight: 100
region: content
hidden: { }

View file

@ -0,0 +1,30 @@
uuid: 26873112-e7a7-4730-8b8a-abe73e29cc9b
langcode: en
status: true
dependencies:
config:
- core.entity_view_mode.node.teaser
- field.field.node.daily_email_cta.body
- node.type.daily_email_cta
module:
- text
- user
id: node.daily_email_cta.teaser
targetEntityType: node
bundle: daily_email_cta
mode: teaser
content:
body:
type: text_summary_or_trimmed
label: hidden
settings:
trim_length: 600
third_party_settings: { }
weight: 0
region: content
links:
settings: { }
third_party_settings: { }
weight: 100
region: content
hidden: { }

View file

@ -0,0 +1,29 @@
uuid: 115fb0c0-03eb-43b1-8b89-3740b2645160
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_daily_email_cta
- node.type.daily_email
- node.type.daily_email_cta
id: node.daily_email.field_daily_email_cta
field_name: field_daily_email_cta
entity_type: node
bundle: daily_email
label: CTA
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings:
handler: 'default:node'
handler_settings:
target_bundles:
daily_email_cta: daily_email_cta
sort:
field: title
direction: ASC
auto_create: false
auto_create_bundle: ''
field_type: entity_reference

View file

@ -0,0 +1,24 @@
uuid: 4bfa25ff-4011-4978-9566-299540a4fc92
langcode: en
status: true
dependencies:
config:
- field.storage.node.body
- node.type.daily_email_cta
module:
- text
id: node.daily_email_cta.body
field_name: body
entity_type: node
bundle: daily_email_cta
label: Body
description: ''
required: true
translatable: false
default_value: { }
default_value_callback: ''
settings:
display_summary: true
required_summary: false
allowed_formats: { }
field_type: text_with_summary

View file

@ -0,0 +1,19 @@
uuid: 1df230c0-e769-4208-88a7-19c756975886
langcode: en
status: true
dependencies:
module:
- node
id: node.field_daily_email_cta
field_name: field_daily_email_cta
entity_type: node
type: entity_reference
settings:
target_type: node
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View file

@ -0,0 +1,17 @@
uuid: aafaaf8f-2670-4202-bc20-2bc31312859b
langcode: en
status: true
dependencies:
module:
- menu_ui
third_party_settings:
menu_ui:
available_menus: { }
parent: ''
name: 'Daily email CTA'
type: daily_email_cta
description: 'A call to action to attach to a daily email.'
help: null
new_revision: true
preview_mode: 1
display_submitted: false

View file

@ -23,6 +23,68 @@ display:
position: 0
display_options:
fields:
body_1:
id: body_1
table: node__body
field: body
relationship: field_daily_email_cta
group_type: group
admin_label: ''
plugin_id: field
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: text_default
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
title:
id: title
table: node_field_data
@ -99,8 +161,8 @@ display:
label: ''
exclude: false
alter:
alter_text: false
text: ''
alter_text: true
text: "{{ body }}\r\n{{ body_1 }}"
make_link: false
path: ''
absolute: false
@ -494,7 +556,16 @@ display:
distinct: false
replica: false
query_tags: { }
relationships: { }
relationships:
field_daily_email_cta:
id: field_daily_email_cta
table: node__field_daily_email_cta
field: field_daily_email_cta
relationship: none
group_type: group
admin_label: 'field_daily_email_cta: Content'
plugin_id: standard
required: false
header: { }
footer: { }
display_extenders: { }

View file

@ -6438,5 +6438,23 @@
],
"path_alias.b9c6a52b-65d7-4122-b132-7b51164a8203": [
"node.0d970bea-36e2-4f0c-b448-4be60af220ef"
],
"node.3074e1e9-c691-4f73-a71c-cfe5920f884e": [
"user.b8966985-d4b2-42a7-a319-2e94ccfbb849"
],
"node.9b4c39a3-702f-486c-a79b-4d7b96a4f3f6": [
"user.b8966985-d4b2-42a7-a319-2e94ccfbb849"
],
"node.20cde1b4-efdc-46a4-a6a4-4fd2264f617e": [
"user.b8966985-d4b2-42a7-a319-2e94ccfbb849"
],
"node.2a6bc8bd-a1e0-4f62-8112-47c3107020c5": [
"user.b8966985-d4b2-42a7-a319-2e94ccfbb849"
],
"node.e3f6c728-7855-4804-8614-e2a0c08c368f": [
"user.b8966985-d4b2-42a7-a319-2e94ccfbb849"
],
"node.c74de3cf-5362-4d08-935a-a9d0d22fcb94": [
"user.b8966985-d4b2-42a7-a319-2e94ccfbb849"
]
}

View file

@ -0,0 +1,92 @@
{
"uuid": [
{
"value": "20cde1b4-efdc-46a4-a6a4-4fd2264f617e"
}
],
"langcode": [
{
"value": "en"
}
],
"type": [
{
"target_id": "daily_email_cta",
"target_type": "node_type",
"target_uuid": "aafaaf8f-2670-4202-bc20-2bc31312859b"
}
],
"revision_timestamp": [
{
"value": "2025-06-13T22:11:07+00:00"
}
],
"revision_uid": [
{
"target_type": "user",
"target_uuid": "b8966985-d4b2-42a7-a319-2e94ccfbb849"
}
],
"revision_log": [],
"status": [
{
"value": true
}
],
"uid": [
{
"target_type": "user",
"target_uuid": "b8966985-d4b2-42a7-a319-2e94ccfbb849"
}
],
"title": [
{
"value": "Open source sponsorship"
}
],
"created": [
{
"value": "2025-06-13T22:10:36+00:00"
}
],
"changed": [
{
"value": "2025-06-13T22:11:07+00:00"
}
],
"promote": [
{
"value": false
}
],
"sticky": [
{
"value": false
}
],
"default_langcode": [
{
"value": true
}
],
"revision_translation_affected": [
{
"value": true
}
],
"path": [
{
"alias": "",
"pid": null,
"langcode": "en"
}
],
"body": [
{
"value": "<p>P.S. Do you want to contribute more to open source but don't have the time? <a href=\"\/sponsor\">Sponsor me to do it for you<\/a>!<\/p>",
"format": "basic_html",
"processed": "<p>P.S. Do you want to contribute more to open source but don't have the time? <a href=\"http:\/\/localhost:8888\/sponsor\">Sponsor me to do it for you<\/a>!<\/p>",
"summary": ""
}
]
}

View file

@ -0,0 +1,92 @@
{
"uuid": [
{
"value": "2a6bc8bd-a1e0-4f62-8112-47c3107020c5"
}
],
"langcode": [
{
"value": "en"
}
],
"type": [
{
"target_id": "daily_email_cta",
"target_type": "node_type",
"target_uuid": "aafaaf8f-2670-4202-bc20-2bc31312859b"
}
],
"revision_timestamp": [
{
"value": "2025-06-13T23:03:11+00:00"
}
],
"revision_uid": [
{
"target_type": "user",
"target_uuid": "b8966985-d4b2-42a7-a319-2e94ccfbb849"
}
],
"revision_log": [],
"status": [
{
"value": true
}
],
"uid": [
{
"target_type": "user",
"target_uuid": "b8966985-d4b2-42a7-a319-2e94ccfbb849"
}
],
"title": [
{
"value": "Subscription"
}
],
"created": [
{
"value": "2025-06-13T23:02:52+00:00"
}
],
"changed": [
{
"value": "2025-06-13T23:03:11+00:00"
}
],
"promote": [
{
"value": false
}
],
"sticky": [
{
"value": false
}
],
"default_langcode": [
{
"value": true
}
],
"revision_translation_affected": [
{
"value": true
}
],
"path": [
{
"alias": "",
"pid": null,
"langcode": "en"
}
],
"body": [
{
"value": "<p>Do you need immediate access to an expert Drupal Developer? With my <a href=\"\/subscription\">Drupal development subscription<\/a>, make unlimited requests for a fixed monthly price in less time than posting to a job board!<\/p>",
"format": "basic_html",
"processed": "<p>Do you need immediate access to an expert Drupal Developer? With my <a href=\"http:\/\/localhost:8888\/subscription\">Drupal development subscription<\/a>, make unlimited requests for a fixed monthly price in less time than posting to a job board!<\/p>",
"summary": ""
}
]
}

View file

@ -0,0 +1,92 @@
{
"uuid": [
{
"value": "3074e1e9-c691-4f73-a71c-cfe5920f884e"
}
],
"langcode": [
{
"value": "en"
}
],
"type": [
{
"target_id": "daily_email_cta",
"target_type": "node_type",
"target_uuid": "aafaaf8f-2670-4202-bc20-2bc31312859b"
}
],
"revision_timestamp": [
{
"value": "2025-06-13T22:10:23+00:00"
}
],
"revision_uid": [
{
"target_type": "user",
"target_uuid": "b8966985-d4b2-42a7-a319-2e94ccfbb849"
}
],
"revision_log": [],
"status": [
{
"value": true
}
],
"uid": [
{
"target_type": "user",
"target_uuid": "b8966985-d4b2-42a7-a319-2e94ccfbb849"
}
],
"title": [
{
"value": "Call"
}
],
"created": [
{
"value": "2025-06-13T22:07:32+00:00"
}
],
"changed": [
{
"value": "2025-06-13T22:10:23+00:00"
}
],
"promote": [
{
"value": false
}
],
"sticky": [
{
"value": false
}
],
"default_langcode": [
{
"value": true
}
],
"revision_translation_affected": [
{
"value": true
}
],
"path": [
{
"alias": "",
"pid": null,
"langcode": "en"
}
],
"body": [
{
"value": "<p>P.S. Are you still using Drupal 7 and don\u2019t know what\u2019s involved to upgrade to Drupal 10? <a href=\"\/call\">Book a Drupal 7 upgrade consultation call<\/a> or <a href=\"\/drupal-upgrade\">an upgrade roadmap<\/a>.<\/p>",
"format": "basic_html",
"processed": "<p>P.S. Are you still using Drupal 7 and don\u2019t know what\u2019s involved to upgrade to Drupal 10? <a href=\"http:\/\/localhost:8888\/call\">Book a Drupal 7 upgrade consultation call<\/a> or <a href=\"http:\/\/localhost:8888\/drupal-upgrade\">an upgrade roadmap<\/a>.<\/p>",
"summary": ""
}
]
}

View file

@ -0,0 +1,92 @@
{
"uuid": [
{
"value": "9b4c39a3-702f-486c-a79b-4d7b96a4f3f6"
}
],
"langcode": [
{
"value": "en"
}
],
"type": [
{
"target_id": "daily_email_cta",
"target_type": "node_type",
"target_uuid": "aafaaf8f-2670-4202-bc20-2bc31312859b"
}
],
"revision_timestamp": [
{
"value": "2025-06-13T22:09:58+00:00"
}
],
"revision_uid": [
{
"target_type": "user",
"target_uuid": "b8966985-d4b2-42a7-a319-2e94ccfbb849"
}
],
"revision_log": [],
"status": [
{
"value": true
}
],
"uid": [
{
"target_type": "user",
"target_uuid": "b8966985-d4b2-42a7-a319-2e94ccfbb849"
}
],
"title": [
{
"value": "Drupal 7 EOL"
}
],
"created": [
{
"value": "2025-06-13T22:08:54+00:00"
}
],
"changed": [
{
"value": "2025-06-13T22:09:58+00:00"
}
],
"promote": [
{
"value": false
}
],
"sticky": [
{
"value": false
}
],
"default_langcode": [
{
"value": true
}
],
"revision_translation_affected": [
{
"value": true
}
],
"path": [
{
"alias": "",
"pid": null,
"langcode": "en"
}
],
"body": [
{
"value": "<p>P.S. Are you stuck on Drupal 7? <a href=\"\/drupal-upgrade\">Plan your upgrade to Drupal 10 now!<\/a><\/p>",
"format": "basic_html",
"processed": "<p>P.S. Are you stuck on Drupal 7? <a href=\"http:\/\/localhost:8888\/drupal-upgrade\">Plan your upgrade to Drupal 10 now!<\/a><\/p>",
"summary": ""
}
]
}

View file

@ -0,0 +1,92 @@
{
"uuid": [
{
"value": "c74de3cf-5362-4d08-935a-a9d0d22fcb94"
}
],
"langcode": [
{
"value": "en"
}
],
"type": [
{
"target_id": "daily_email_cta",
"target_type": "node_type",
"target_uuid": "aafaaf8f-2670-4202-bc20-2bc31312859b"
}
],
"revision_timestamp": [
{
"value": "2025-06-13T23:04:29+00:00"
}
],
"revision_uid": [
{
"target_type": "user",
"target_uuid": "b8966985-d4b2-42a7-a319-2e94ccfbb849"
}
],
"revision_log": [],
"status": [
{
"value": true
}
],
"uid": [
{
"target_type": "user",
"target_uuid": "b8966985-d4b2-42a7-a319-2e94ccfbb849"
}
],
"title": [
{
"value": "Pair programming"
}
],
"created": [
{
"value": "2025-06-13T23:04:05+00:00"
}
],
"changed": [
{
"value": "2025-06-13T23:04:29+00:00"
}
],
"promote": [
{
"value": false
}
],
"sticky": [
{
"value": false
}
],
"default_langcode": [
{
"value": true
}
],
"revision_translation_affected": [
{
"value": true
}
],
"path": [
{
"alias": "",
"pid": null,
"langcode": "en"
}
],
"body": [
{
"value": "<p>Need help or want another pair of eyes on your code? Book a <a href=\"\/call\">1-on-1 consulting call<\/a> or an <a href=\"\/pair\">online pair programming session<\/a> with a 100% money-back guarantee.,<\/p>",
"format": "basic_html",
"processed": "<p>Need help or want another pair of eyes on your code? Book a <a href=\"http:\/\/localhost:8888\/call\">1-on-1 consulting call<\/a> or an <a href=\"http:\/\/localhost:8888\/pair\">online pair programming session<\/a> with a 100% money-back guarantee.,<\/p>",
"summary": ""
}
]
}

View file

@ -0,0 +1,92 @@
{
"uuid": [
{
"value": "e3f6c728-7855-4804-8614-e2a0c08c368f"
}
],
"langcode": [
{
"value": "en"
}
],
"type": [
{
"target_id": "daily_email_cta",
"target_type": "node_type",
"target_uuid": "aafaaf8f-2670-4202-bc20-2bc31312859b"
}
],
"revision_timestamp": [
{
"value": "2025-06-13T23:03:45+00:00"
}
],
"revision_uid": [
{
"target_type": "user",
"target_uuid": "b8966985-d4b2-42a7-a319-2e94ccfbb849"
}
],
"revision_log": [],
"status": [
{
"value": true
}
],
"uid": [
{
"target_type": "user",
"target_uuid": "b8966985-d4b2-42a7-a319-2e94ccfbb849"
}
],
"title": [
{
"value": "Drupal testing course"
}
],
"created": [
{
"value": "2025-06-13T23:03:26+00:00"
}
],
"changed": [
{
"value": "2025-06-13T23:03:45+00:00"
}
],
"promote": [
{
"value": false
}
],
"sticky": [
{
"value": false
}
],
"default_langcode": [
{
"value": true
}
],
"revision_translation_affected": [
{
"value": true
}
],
"path": [
{
"alias": "",
"pid": null,
"langcode": "en"
}
],
"body": [
{
"value": "<p>Do you want to learn about automated testing in Drupal? Take my <a href=\"\/atdc\">free 10-day email course<\/a> and get daily lessons straight to your inbox.<\/p>",
"format": "basic_html",
"processed": "<p>Do you want to learn about automated testing in Drupal? Take my <a href=\"http:\/\/localhost:8888\/atdc\">free 10-day email course<\/a> and get daily lessons straight to your inbox.<\/p>",
"summary": ""
}
]
}

View file

@ -4,8 +4,54 @@ declare(strict_types=1);
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\node\NodeInterface;
use Drupal\opd_daily_emails\Ctas;
use Drupal\opd_daily_emails\DailyEmail;
use Drupal\opd_daily_emails\DailyEmailNodeRepository;
/**
* Implements hook_entity_bundle_info_alter().
*
* @param array<non-empty-string, array{class: non-empty-string}> $bundles
*/
function opd_daily_emails_entity_bundle_info_alter(array &$bundles): void {
if (isset($bundles['node'])) {
$bundles['node'][DailyEmail::NODE_TYPE]['class'] = DailyEmail::class;
}
}
/**
* Implements hook_entity_presave().
*/
function opd_daily_emails_entity_presave(Drupal\Core\Entity\EntityInterface $entity): void {
if (!$entity instanceof DailyEmail) {
return;
}
if (!$entity->isNew()) {
return;
}
if ($entity->get('field_daily_email_cta')->getValue() !== []) {
return;
}
$nodeStorage = \Drupal::entityTypeManager()->getStorage('node');
$query = $nodeStorage->getQuery();
$query->condition('status', NodeInterface::PUBLISHED);
$query->condition('type', 'daily_email_cta');
$query->accessCheck();
$ctaNodes = $nodeStorage->loadMultiple($query->execute());
$ctas = Ctas::fromNodes($ctaNodes);
if ($ctas->isEmpty()) {
return;
}
$entity->set('field_daily_email_cta', $ctas->getRandomCta());
}
/**
* Implements hook_token_info().
*

View file

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Drupal\opd_daily_emails;
use Drupal\node\NodeInterface;
use Webmozart\Assert\Assert;
final class Ctas {
public function isEmpty(): bool {
return empty($this->ctas);
}
public function getRandomCta(): NodeInterface {
return $this->ctas[array_rand($this->ctas)];
}
/**
* @param list<NodeInterface> $nodes
*/
public static function fromNodes(array $nodes): self {
return new self($nodes);
}
/**
* @param list<NodeInterface> $ctas
*/
private function __construct(
private array $ctas,
) {
Assert::allIsInstanceOf($ctas, NodeInterface::class);
}
}

View file

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\opd_daily_emails;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
final class DailyEmail extends Node implements NodeInterface {
public const NODE_TYPE = 'daily_email';
}

View file

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\opd_daily_emails\Functional;
use weitzman\DrupalTestTraits\ExistingSiteBase;
final class CallToActionTest extends ExistingSiteBase {
public function test_saving_an_email_node_without_a_cta_will_populate_one(): void {
$cta = $this->createNode([
'type' => 'daily_email_cta',
]);
$email = $this->createNode([
'field_daily_email_cta' => NULL,
'type' => 'daily_email',
]);
$this->assertNotEmpty($email->get('field_daily_email_cta')->getValue());
// TODO: assert the returned text.
}
public function test_saving_an_email_node_with_a_cta_will_keep_the_same_cta(): void {
$cta = $this->createNode([
'type' => 'daily_email_cta',
]);
$email = $this->createNode([
'field_daily_email_cta' => $cta,
'type' => 'daily_email',
]);
$email->set('title', 'Updated');
$email->save();
$this->assertSame(
actual: $email->get('field_daily_email_cta')->getValue()[0]['target_id'],
expected: $cta->id(),
);
}
}