diff --git a/bedrock/mozorg/blocks/advertising.py b/bedrock/mozorg/blocks/advertising.py
index 70ece5f637f..b3e83490e83 100644
--- a/bedrock/mozorg/blocks/advertising.py
+++ b/bedrock/mozorg/blocks/advertising.py
@@ -51,7 +51,7 @@ class SectionHeaderBlock(blocks.StructBlock):
)
superheading_text = blocks.CharBlock(char_max_length=255, required=False)
heading_text = blocks.CharBlock(char_max_length=255, required=False)
- subheading_text = blocks.CharBlock(char_max_length=255, required=False)
+ subheading_text = blocks.RichTextBlock(required=False)
image = ImageChooserBlock(required=False)
def clean(self, value):
@@ -59,7 +59,7 @@ def clean(self, value):
superheading = value.get("superheading_text", "").strip()
heading = value.get("heading_text", "").strip()
- subheading = value.get("subheading_text", "").strip()
+ subheading = value.get("subheading_text") # RichText (or None); falsy when it has no source
if not superheading and not heading and not subheading:
errors["heading_text"] = ValidationError("You must provide either superheading, or heading, or subheading.")
@@ -317,7 +317,8 @@ class SectionBlock(blocks.StructBlock):
("statistic_callout_block", StatisticCalloutBlock()),
("features_with_modals", FeatureListWithModalsBlock()),
("feature_list_block", FeatureListBlock()),
- ]
+ ],
+ required=False,
)
call_to_action = blocks.ListBlock(RowTextAndLinkBlock(), min_num=0, max_num=1, default=[], label="Call to Action")
diff --git a/bedrock/mozorg/migrations/0056_alter_advertisingindexpage_sections_and_more.py b/bedrock/mozorg/migrations/0056_alter_advertisingindexpage_sections_and_more.py
new file mode 100644
index 00000000000..33c899c6447
--- /dev/null
+++ b/bedrock/mozorg/migrations/0056_alter_advertisingindexpage_sections_and_more.py
@@ -0,0 +1,29 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+# Generated by Django 5.2.18 on 2026-10-27 20:28
+
+import wagtail.admin.forms.choosers
+import wagtail.fields
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('mozorg', '0055_prose_rich_text_and_heading_size'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='advertisingindexpage',
+ name='sections',
+ field=wagtail.fields.StreamField([('section', 40)], blank=True, block_lookup={0: ('wagtail.blocks.CharBlock', (), {'help_text': "Optional: Add an ID to make this section linkable from navigation (e.g., 'solutions', 'why-mozilla')", 'max_length': 100, 'required': False}), 1: ('wagtail.blocks.BooleanBlock', (), {'default': False, 'inline_form': True, 'label': 'Should the section have a divider line on top?', 'required': False}), 2: ('wagtail.blocks.BooleanBlock', (), {'default': False, 'inline_form': True, 'label': 'Should the section have a dark background?', 'required': False}), 3: ('wagtail.blocks.StructBlock', [[('anchor_id', 0), ('has_top_divider', 1), ('display_on_dark_background', 2)]], {}), 4: ('wagtail.blocks.CharBlock', (), {'char_max_length': 255, 'required': False}), 5: ('wagtail.blocks.RichTextBlock', (), {'required': False}), 6: ('wagtail.images.blocks.ImageChooserBlock', (), {'required': False}), 7: ('wagtail.blocks.StructBlock', [[('anchor_id', 0), ('superheading_text', 4), ('heading_text', 4), ('subheading_text', 5), ('image', 6)]], {}), 8: ('wagtail.blocks.CharBlock', (), {'help_text': 'Optional: Add an ID to make this section linkable from navigation', 'max_length': 100, 'required': False}), 9: ('wagtail.blocks.RichTextBlock', (), {'char_max_length': 255, 'required': False}), 10: ('wagtail.blocks.CharBlock', (), {'char_max_length': 255}), 11: ('wagtail.blocks.BooleanBlock', (), {'default': False, 'inline_form': True, 'label': 'Position the image to the right of the statistic', 'required': False}), 12: ('wagtail.blocks.StructBlock', [[('anchor_id', 8), ('image', 6), ('image_caption', 9), ('statistic_value', 10), ('statistic_label', 10), ('align_image_on_right', 11)]], {}), 13: ('wagtail.blocks.StructBlock', [[('statistic_superlabel', 4), ('statistic_value', 10), ('statistic_label', 10)]], {}), 14: ('wagtail.blocks.ListBlock', (13,), {'min_num': 1}), 15: ('wagtail.blocks.StructBlock', [[('anchor_id', 8), ('heading_text', 10), ('statistics', 14)]], {}), 16: ('wagtail.blocks.TextBlock', (), {}), 17: ('wagtail.blocks.TextBlock', (), {'char_max_length': 255, 'required': False}), 18: ('wagtail.blocks.StructBlock', [[('image', 6), ('image_caption_heading', 17), ('image_caption_text', 17)]], {}), 19: ('wagtail.blocks.ListBlock', (18,), {'min_num': 1}), 20: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('page', 'Page'), ('file', 'File'), ('custom_url', 'Custom URL'), ('email', 'Email'), ('anchor', 'Anchor'), ('phone', 'Phone')], 'classname': 'link_choice_type_selector', 'label': 'Link to', 'required': False}), 21: ('wagtail.blocks.PageChooserBlock', (), {'form_classname': 'page_link', 'label': 'Page', 'required': False}), 22: ('wagtail.documents.blocks.DocumentChooserBlock', (), {'form_classname': 'file_link', 'label': 'File', 'required': False}), 23: ('wagtail.blocks.CharBlock', (), {'form_classname': 'custom_url_link url_field', 'label': 'Custom URL', 'max_length': 300, 'required': False, 'validators': [wagtail.admin.forms.choosers.URLOrAbsolutePathValidator()]}), 24: ('wagtail.blocks.CharBlock', (), {'form_classname': 'anchor_link', 'label': '#', 'max_length': 300, 'required': False}), 25: ('wagtail.blocks.EmailBlock', (), {'required': False}), 26: ('wagtail.blocks.CharBlock', (), {'form_classname': 'phone_link', 'label': 'Phone', 'max_length': 30, 'required': False}), 27: ('wagtail.blocks.BooleanBlock', (), {'form_classname': 'new_window_toggle', 'label': 'Open in new window', 'required': False}), 28: ('wagtail.blocks.StructBlock', [[('link_to', 20), ('page', 21), ('file', 22), ('custom_url', 23), ('anchor', 24), ('email', 25), ('phone', 26), ('new_window', 27)]], {'label': 'CTA Link', 'required': False}), 29: ('wagtail.blocks.StructBlock', [[('heading_text', 10), ('figures', 19), ('cta_text', 4), ('cta_link', 28)]], {}), 30: ('wagtail.blocks.ListBlock', (29,), {'min_num': 1}), 31: ('wagtail.blocks.StructBlock', [[('anchor_id', 8), ('heading_text', 10), ('supporting_text', 16), ('feature_list_items', 30)]], {}), 32: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link']}), 33: ('wagtail.blocks.StructBlock', [[('heading_text', 16), ('supporting_text', 32)]], {}), 34: ('wagtail.blocks.ListBlock', (33,), {'min_num': 1}), 35: ('wagtail.blocks.StructBlock', [[('anchor_id', 8), ('feature_list_items', 34)]], {}), 36: ('wagtail.blocks.StreamBlock', [[('section_header_block', 7), ('figure_with_statistic_block', 12), ('statistic_callout_block', 15), ('features_with_modals', 31), ('feature_list_block', 35)]], {'required': False}), 37: ('wagtail.blocks.StructBlock', [[('link_to', 20), ('page', 21), ('file', 22), ('custom_url', 23), ('anchor', 24), ('email', 25), ('phone', 26), ('new_window', 27)]], {'label': 'Link'}), 38: ('wagtail.blocks.StructBlock', [[('text', 4), ('link_text', 10), ('link', 37)]], {}), 39: ('wagtail.blocks.ListBlock', (38,), {'default': [], 'label': 'Call to Action', 'max_num': 1, 'min_num': 0}), 40: ('wagtail.blocks.StructBlock', [[('settings', 3), ('header', 7), ('content', 36), ('call_to_action', 39)]], {})}, null=True),
+ ),
+ migrations.AlterField(
+ model_name='contentsubpage',
+ name='sections',
+ field=wagtail.fields.StreamField([('section', 40)], blank=True, block_lookup={0: ('wagtail.blocks.CharBlock', (), {'help_text': "Optional: Add an ID to make this section linkable from navigation (e.g., 'solutions', 'why-mozilla')", 'max_length': 100, 'required': False}), 1: ('wagtail.blocks.BooleanBlock', (), {'default': False, 'inline_form': True, 'label': 'Should the section have a divider line on top?', 'required': False}), 2: ('wagtail.blocks.BooleanBlock', (), {'default': False, 'inline_form': True, 'label': 'Should the section have a dark background?', 'required': False}), 3: ('wagtail.blocks.StructBlock', [[('anchor_id', 0), ('has_top_divider', 1), ('display_on_dark_background', 2)]], {}), 4: ('wagtail.blocks.CharBlock', (), {'char_max_length': 255, 'required': False}), 5: ('wagtail.blocks.RichTextBlock', (), {'required': False}), 6: ('wagtail.images.blocks.ImageChooserBlock', (), {'required': False}), 7: ('wagtail.blocks.StructBlock', [[('anchor_id', 0), ('superheading_text', 4), ('heading_text', 4), ('subheading_text', 5), ('image', 6)]], {}), 8: ('wagtail.blocks.CharBlock', (), {'help_text': 'Optional: Add an ID to make this section linkable from navigation', 'max_length': 100, 'required': False}), 9: ('wagtail.blocks.RichTextBlock', (), {'char_max_length': 255, 'required': False}), 10: ('wagtail.blocks.CharBlock', (), {'char_max_length': 255}), 11: ('wagtail.blocks.BooleanBlock', (), {'default': False, 'inline_form': True, 'label': 'Position the image to the right of the statistic', 'required': False}), 12: ('wagtail.blocks.StructBlock', [[('anchor_id', 8), ('image', 6), ('image_caption', 9), ('statistic_value', 10), ('statistic_label', 10), ('align_image_on_right', 11)]], {}), 13: ('wagtail.blocks.StructBlock', [[('statistic_superlabel', 4), ('statistic_value', 10), ('statistic_label', 10)]], {}), 14: ('wagtail.blocks.ListBlock', (13,), {'min_num': 1}), 15: ('wagtail.blocks.StructBlock', [[('anchor_id', 8), ('heading_text', 10), ('statistics', 14)]], {}), 16: ('wagtail.blocks.TextBlock', (), {}), 17: ('wagtail.blocks.TextBlock', (), {'char_max_length': 255, 'required': False}), 18: ('wagtail.blocks.StructBlock', [[('image', 6), ('image_caption_heading', 17), ('image_caption_text', 17)]], {}), 19: ('wagtail.blocks.ListBlock', (18,), {'min_num': 1}), 20: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('page', 'Page'), ('file', 'File'), ('custom_url', 'Custom URL'), ('email', 'Email'), ('anchor', 'Anchor'), ('phone', 'Phone')], 'classname': 'link_choice_type_selector', 'label': 'Link to', 'required': False}), 21: ('wagtail.blocks.PageChooserBlock', (), {'form_classname': 'page_link', 'label': 'Page', 'required': False}), 22: ('wagtail.documents.blocks.DocumentChooserBlock', (), {'form_classname': 'file_link', 'label': 'File', 'required': False}), 23: ('wagtail.blocks.CharBlock', (), {'form_classname': 'custom_url_link url_field', 'label': 'Custom URL', 'max_length': 300, 'required': False, 'validators': [wagtail.admin.forms.choosers.URLOrAbsolutePathValidator()]}), 24: ('wagtail.blocks.CharBlock', (), {'form_classname': 'anchor_link', 'label': '#', 'max_length': 300, 'required': False}), 25: ('wagtail.blocks.EmailBlock', (), {'required': False}), 26: ('wagtail.blocks.CharBlock', (), {'form_classname': 'phone_link', 'label': 'Phone', 'max_length': 30, 'required': False}), 27: ('wagtail.blocks.BooleanBlock', (), {'form_classname': 'new_window_toggle', 'label': 'Open in new window', 'required': False}), 28: ('wagtail.blocks.StructBlock', [[('link_to', 20), ('page', 21), ('file', 22), ('custom_url', 23), ('anchor', 24), ('email', 25), ('phone', 26), ('new_window', 27)]], {'label': 'CTA Link', 'required': False}), 29: ('wagtail.blocks.StructBlock', [[('heading_text', 10), ('figures', 19), ('cta_text', 4), ('cta_link', 28)]], {}), 30: ('wagtail.blocks.ListBlock', (29,), {'min_num': 1}), 31: ('wagtail.blocks.StructBlock', [[('anchor_id', 8), ('heading_text', 10), ('supporting_text', 16), ('feature_list_items', 30)]], {}), 32: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link']}), 33: ('wagtail.blocks.StructBlock', [[('heading_text', 16), ('supporting_text', 32)]], {}), 34: ('wagtail.blocks.ListBlock', (33,), {'min_num': 1}), 35: ('wagtail.blocks.StructBlock', [[('anchor_id', 8), ('feature_list_items', 34)]], {}), 36: ('wagtail.blocks.StreamBlock', [[('section_header_block', 7), ('figure_with_statistic_block', 12), ('statistic_callout_block', 15), ('features_with_modals', 31), ('feature_list_block', 35)]], {'required': False}), 37: ('wagtail.blocks.StructBlock', [[('link_to', 20), ('page', 21), ('file', 22), ('custom_url', 23), ('anchor', 24), ('email', 25), ('phone', 26), ('new_window', 27)]], {'label': 'Link'}), 38: ('wagtail.blocks.StructBlock', [[('text', 4), ('link_text', 10), ('link', 37)]], {}), 39: ('wagtail.blocks.ListBlock', (38,), {'default': [], 'label': 'Call to Action', 'max_num': 1, 'min_num': 0}), 40: ('wagtail.blocks.StructBlock', [[('settings', 3), ('header', 7), ('content', 36), ('call_to_action', 39)]], {})}, null=True),
+ ),
+ ]
diff --git a/bedrock/mozorg/templates/mozorg/cms/advertising/blocks/section_heading_block.html b/bedrock/mozorg/templates/mozorg/cms/advertising/blocks/section_heading_block.html
index 33a1492689b..5dea9f913d6 100644
--- a/bedrock/mozorg/templates/mozorg/cms/advertising/blocks/section_heading_block.html
+++ b/bedrock/mozorg/templates/mozorg/cms/advertising/blocks/section_heading_block.html
@@ -10,7 +10,7 @@
{% if value.superheading_text %} {{ value.superheading_text }} {{ value.subheading_text }}{{ value.heading_text }}
{% endif %}
- {% if value.subheading_text %}
Test subheading
"), ), ), ) @@ -96,6 +98,19 @@ def test_advertising_index_page(minimal_site, rf, serving_method): # noqa assert "Follow Mozilla Ads to get the latest secure advertising trends." in page_content +def test_section_header_block_requires_a_heading(): + block = SectionHeaderBlock() + + # All heading fields empty -> validation error (covers the RichText .source path) + empty = block.to_python({"superheading_text": "", "heading_text": "", "subheading_text": ""}) + with pytest.raises(blocks.StructBlockValidationError): + block.clean(empty) + + # A rich-text subheading alone is enough to pass + with_subheading = block.to_python({"superheading_text": "", "heading_text": "", "subheading_text": "Hi
"}) + block.clean(with_subheading) # should not raise + + @pytest.mark.parametrize("serving_method", ("serve", "serve_preview")) def test_two_column_subpage(minimal_site, rf, serving_method): # noqa root_page = minimal_site.root_page