Sfoglia il codice sorgente

Expand rich text html

Sage Abdullah 2 mesi fa
parent
commit
354dbc7042

+ 7 - 1
bakerydemo/base/blocks.py

@@ -10,6 +10,7 @@ from wagtail.blocks import (
 from wagtail.embeds.blocks import EmbedBlock
 from wagtail.images import get_image_model
 from wagtail.images.blocks import ImageChooserBlock
+from wagtail.rich_text import expand_db_html
 
 
 def get_image_api_representation(image):
@@ -107,6 +108,11 @@ class CustomEmbedBlock(EmbedBlock):
         return {"url": value.url, "html": value.html}
 
 
+class CustomRichTextBlock(RichTextBlock):
+    def get_api_representation(self, value, context=None):
+        return expand_db_html(super().get_api_representation(value, context))
+
+
 # StreamBlocks
 class BaseStreamBlock(StreamBlock):
     """
@@ -114,7 +120,7 @@ class BaseStreamBlock(StreamBlock):
     """
 
     heading_block = HeadingBlock()
-    paragraph_block = RichTextBlock(
+    paragraph_block = CustomRichTextBlock(
         icon="pilcrow",
         template="blocks/paragraph_block.html",
         preview_value=(

+ 352 - 0
bakerydemo/base/migrations/0025_alter_formpage_body_alter_gallerypage_body_and_more.py

@@ -0,0 +1,352 @@
+# Generated by Django 5.1.8 on 2025-06-19 13:28
+
+import wagtail.fields
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("base", "0024_alter_formpage_body_alter_gallerypage_body_and_more"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="formpage",
+            name="body",
+            field=wagtail.fields.StreamField(
+                [
+                    ("heading_block", 2),
+                    ("paragraph_block", 3),
+                    ("image_block", 6),
+                    ("block_quote", 9),
+                    ("embed_block", 10),
+                ],
+                block_lookup={
+                    0: (
+                        "wagtail.blocks.CharBlock",
+                        (),
+                        {"form_classname": "title", "required": True},
+                    ),
+                    1: (
+                        "wagtail.blocks.ChoiceBlock",
+                        [],
+                        {
+                            "blank": True,
+                            "choices": [
+                                ("", "Select a header size"),
+                                ("h2", "H2"),
+                                ("h3", "H3"),
+                                ("h4", "H4"),
+                            ],
+                            "required": False,
+                        },
+                    ),
+                    2: (
+                        "wagtail.blocks.StructBlock",
+                        [[("heading_text", 0), ("size", 1)]],
+                        {},
+                    ),
+                    3: (
+                        "bakerydemo.base.blocks.CustomRichTextBlock",
+                        (),
+                        {
+                            "description": "A rich text paragraph",
+                            "icon": "pilcrow",
+                            "preview_value": '\n            <h2>Our bread pledge</h2>\n            <p>As a bakery, <b>breads</b> have <i>always</i> been in our hearts.\n            <a href="https://en.wikipedia.org/wiki/Staple_food">Staple foods</a>\n            are essential for society, and – bread is the tastiest of all.\n            We love to transform batters and doughs into baked goods with a firm\n            dry crust and fluffy center.</p>\n            ',
+                            "template": "blocks/paragraph_block.html",
+                        },
+                    ),
+                    4: (
+                        "wagtail.images.blocks.ImageChooserBlock",
+                        (),
+                        {"required": True},
+                    ),
+                    5: ("wagtail.blocks.CharBlock", (), {"required": False}),
+                    6: (
+                        "wagtail.blocks.StructBlock",
+                        [[("image", 4), ("caption", 5), ("attribution", 5)]],
+                        {},
+                    ),
+                    7: ("wagtail.blocks.TextBlock", (), {}),
+                    8: (
+                        "wagtail.blocks.CharBlock",
+                        (),
+                        {"blank": True, "label": "e.g. Mary Berry", "required": False},
+                    ),
+                    9: (
+                        "wagtail.blocks.StructBlock",
+                        [[("text", 7), ("attribute_name", 8)]],
+                        {},
+                    ),
+                    10: (
+                        "bakerydemo.base.blocks.CustomEmbedBlock",
+                        (),
+                        {
+                            "description": "An embedded video or other media",
+                            "help_text": "Insert an embed URL e.g https://www.youtube.com/watch?v=SGJFWirQ3ks",
+                            "icon": "media",
+                            "preview_template": "base/preview/static_embed_block.html",
+                            "preview_value": "https://www.youtube.com/watch?v=mwrGSfiB1Mg",
+                            "template": "blocks/embed_block.html",
+                        },
+                    ),
+                },
+            ),
+        ),
+        migrations.AlterField(
+            model_name="gallerypage",
+            name="body",
+            field=wagtail.fields.StreamField(
+                [
+                    ("heading_block", 2),
+                    ("paragraph_block", 3),
+                    ("image_block", 6),
+                    ("block_quote", 9),
+                    ("embed_block", 10),
+                ],
+                blank=True,
+                block_lookup={
+                    0: (
+                        "wagtail.blocks.CharBlock",
+                        (),
+                        {"form_classname": "title", "required": True},
+                    ),
+                    1: (
+                        "wagtail.blocks.ChoiceBlock",
+                        [],
+                        {
+                            "blank": True,
+                            "choices": [
+                                ("", "Select a header size"),
+                                ("h2", "H2"),
+                                ("h3", "H3"),
+                                ("h4", "H4"),
+                            ],
+                            "required": False,
+                        },
+                    ),
+                    2: (
+                        "wagtail.blocks.StructBlock",
+                        [[("heading_text", 0), ("size", 1)]],
+                        {},
+                    ),
+                    3: (
+                        "bakerydemo.base.blocks.CustomRichTextBlock",
+                        (),
+                        {
+                            "description": "A rich text paragraph",
+                            "icon": "pilcrow",
+                            "preview_value": '\n            <h2>Our bread pledge</h2>\n            <p>As a bakery, <b>breads</b> have <i>always</i> been in our hearts.\n            <a href="https://en.wikipedia.org/wiki/Staple_food">Staple foods</a>\n            are essential for society, and – bread is the tastiest of all.\n            We love to transform batters and doughs into baked goods with a firm\n            dry crust and fluffy center.</p>\n            ',
+                            "template": "blocks/paragraph_block.html",
+                        },
+                    ),
+                    4: (
+                        "wagtail.images.blocks.ImageChooserBlock",
+                        (),
+                        {"required": True},
+                    ),
+                    5: ("wagtail.blocks.CharBlock", (), {"required": False}),
+                    6: (
+                        "wagtail.blocks.StructBlock",
+                        [[("image", 4), ("caption", 5), ("attribution", 5)]],
+                        {},
+                    ),
+                    7: ("wagtail.blocks.TextBlock", (), {}),
+                    8: (
+                        "wagtail.blocks.CharBlock",
+                        (),
+                        {"blank": True, "label": "e.g. Mary Berry", "required": False},
+                    ),
+                    9: (
+                        "wagtail.blocks.StructBlock",
+                        [[("text", 7), ("attribute_name", 8)]],
+                        {},
+                    ),
+                    10: (
+                        "bakerydemo.base.blocks.CustomEmbedBlock",
+                        (),
+                        {
+                            "description": "An embedded video or other media",
+                            "help_text": "Insert an embed URL e.g https://www.youtube.com/watch?v=SGJFWirQ3ks",
+                            "icon": "media",
+                            "preview_template": "base/preview/static_embed_block.html",
+                            "preview_value": "https://www.youtube.com/watch?v=mwrGSfiB1Mg",
+                            "template": "blocks/embed_block.html",
+                        },
+                    ),
+                },
+                verbose_name="Page body",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="homepage",
+            name="body",
+            field=wagtail.fields.StreamField(
+                [
+                    ("heading_block", 2),
+                    ("paragraph_block", 3),
+                    ("image_block", 6),
+                    ("block_quote", 9),
+                    ("embed_block", 10),
+                ],
+                blank=True,
+                block_lookup={
+                    0: (
+                        "wagtail.blocks.CharBlock",
+                        (),
+                        {"form_classname": "title", "required": True},
+                    ),
+                    1: (
+                        "wagtail.blocks.ChoiceBlock",
+                        [],
+                        {
+                            "blank": True,
+                            "choices": [
+                                ("", "Select a header size"),
+                                ("h2", "H2"),
+                                ("h3", "H3"),
+                                ("h4", "H4"),
+                            ],
+                            "required": False,
+                        },
+                    ),
+                    2: (
+                        "wagtail.blocks.StructBlock",
+                        [[("heading_text", 0), ("size", 1)]],
+                        {},
+                    ),
+                    3: (
+                        "bakerydemo.base.blocks.CustomRichTextBlock",
+                        (),
+                        {
+                            "description": "A rich text paragraph",
+                            "icon": "pilcrow",
+                            "preview_value": '\n            <h2>Our bread pledge</h2>\n            <p>As a bakery, <b>breads</b> have <i>always</i> been in our hearts.\n            <a href="https://en.wikipedia.org/wiki/Staple_food">Staple foods</a>\n            are essential for society, and – bread is the tastiest of all.\n            We love to transform batters and doughs into baked goods with a firm\n            dry crust and fluffy center.</p>\n            ',
+                            "template": "blocks/paragraph_block.html",
+                        },
+                    ),
+                    4: (
+                        "wagtail.images.blocks.ImageChooserBlock",
+                        (),
+                        {"required": True},
+                    ),
+                    5: ("wagtail.blocks.CharBlock", (), {"required": False}),
+                    6: (
+                        "wagtail.blocks.StructBlock",
+                        [[("image", 4), ("caption", 5), ("attribution", 5)]],
+                        {},
+                    ),
+                    7: ("wagtail.blocks.TextBlock", (), {}),
+                    8: (
+                        "wagtail.blocks.CharBlock",
+                        (),
+                        {"blank": True, "label": "e.g. Mary Berry", "required": False},
+                    ),
+                    9: (
+                        "wagtail.blocks.StructBlock",
+                        [[("text", 7), ("attribute_name", 8)]],
+                        {},
+                    ),
+                    10: (
+                        "bakerydemo.base.blocks.CustomEmbedBlock",
+                        (),
+                        {
+                            "description": "An embedded video or other media",
+                            "help_text": "Insert an embed URL e.g https://www.youtube.com/watch?v=SGJFWirQ3ks",
+                            "icon": "media",
+                            "preview_template": "base/preview/static_embed_block.html",
+                            "preview_value": "https://www.youtube.com/watch?v=mwrGSfiB1Mg",
+                            "template": "blocks/embed_block.html",
+                        },
+                    ),
+                },
+                verbose_name="Home content block",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="standardpage",
+            name="body",
+            field=wagtail.fields.StreamField(
+                [
+                    ("heading_block", 2),
+                    ("paragraph_block", 3),
+                    ("image_block", 6),
+                    ("block_quote", 9),
+                    ("embed_block", 10),
+                ],
+                blank=True,
+                block_lookup={
+                    0: (
+                        "wagtail.blocks.CharBlock",
+                        (),
+                        {"form_classname": "title", "required": True},
+                    ),
+                    1: (
+                        "wagtail.blocks.ChoiceBlock",
+                        [],
+                        {
+                            "blank": True,
+                            "choices": [
+                                ("", "Select a header size"),
+                                ("h2", "H2"),
+                                ("h3", "H3"),
+                                ("h4", "H4"),
+                            ],
+                            "required": False,
+                        },
+                    ),
+                    2: (
+                        "wagtail.blocks.StructBlock",
+                        [[("heading_text", 0), ("size", 1)]],
+                        {},
+                    ),
+                    3: (
+                        "bakerydemo.base.blocks.CustomRichTextBlock",
+                        (),
+                        {
+                            "description": "A rich text paragraph",
+                            "icon": "pilcrow",
+                            "preview_value": '\n            <h2>Our bread pledge</h2>\n            <p>As a bakery, <b>breads</b> have <i>always</i> been in our hearts.\n            <a href="https://en.wikipedia.org/wiki/Staple_food">Staple foods</a>\n            are essential for society, and – bread is the tastiest of all.\n            We love to transform batters and doughs into baked goods with a firm\n            dry crust and fluffy center.</p>\n            ',
+                            "template": "blocks/paragraph_block.html",
+                        },
+                    ),
+                    4: (
+                        "wagtail.images.blocks.ImageChooserBlock",
+                        (),
+                        {"required": True},
+                    ),
+                    5: ("wagtail.blocks.CharBlock", (), {"required": False}),
+                    6: (
+                        "wagtail.blocks.StructBlock",
+                        [[("image", 4), ("caption", 5), ("attribution", 5)]],
+                        {},
+                    ),
+                    7: ("wagtail.blocks.TextBlock", (), {}),
+                    8: (
+                        "wagtail.blocks.CharBlock",
+                        (),
+                        {"blank": True, "label": "e.g. Mary Berry", "required": False},
+                    ),
+                    9: (
+                        "wagtail.blocks.StructBlock",
+                        [[("text", 7), ("attribute_name", 8)]],
+                        {},
+                    ),
+                    10: (
+                        "bakerydemo.base.blocks.CustomEmbedBlock",
+                        (),
+                        {
+                            "description": "An embedded video or other media",
+                            "help_text": "Insert an embed URL e.g https://www.youtube.com/watch?v=SGJFWirQ3ks",
+                            "icon": "media",
+                            "preview_template": "base/preview/static_embed_block.html",
+                            "preview_value": "https://www.youtube.com/watch?v=mwrGSfiB1Mg",
+                            "template": "blocks/embed_block.html",
+                        },
+                    ),
+                },
+                verbose_name="Page body",
+            ),
+        ),
+    ]

+ 4 - 3
bakerydemo/base/models.py

@@ -37,6 +37,7 @@ from wagtail.models import (
 from wagtail.search import index
 
 from bakerydemo.headless import CustomHeadlessMixin
+from bakerydemo.serializers import RichTextSerializer
 
 from .blocks import BaseStreamBlock
 
@@ -211,7 +212,7 @@ class FooterText(
     ]
 
     api_fields = [
-        APIField("body"),
+        APIField("body", serializer=RichTextSerializer()),
     ]
 
     def __str__(self):
@@ -427,7 +428,7 @@ class HomePage(CustomHeadlessMixin, Page):
         APIField("body"),
         APIField("promo_image"),
         APIField("promo_title"),
-        APIField("promo_text"),
+        APIField("promo_text", serializer=RichTextSerializer()),
         APIField("featured_section_1_title"),
         APIField("featured_section_1"),
         APIField("featured_section_2_title"),
@@ -541,7 +542,7 @@ class FormPage(CustomHeadlessMixin, AbstractEmailForm):
         APIField("subject"),
         APIField("image"),
         APIField("body"),
-        APIField("thank_you_text"),
+        APIField("thank_you_text", serializer=RichTextSerializer()),
     ]
 
 

+ 99 - 0
bakerydemo/blog/migrations/0008_alter_blogpage_body.py

@@ -0,0 +1,99 @@
+# Generated by Django 5.1.8 on 2025-06-19 13:28
+
+import wagtail.fields
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("blog", "0007_alter_blogpage_body"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="blogpage",
+            name="body",
+            field=wagtail.fields.StreamField(
+                [
+                    ("heading_block", 2),
+                    ("paragraph_block", 3),
+                    ("image_block", 6),
+                    ("block_quote", 9),
+                    ("embed_block", 10),
+                ],
+                blank=True,
+                block_lookup={
+                    0: (
+                        "wagtail.blocks.CharBlock",
+                        (),
+                        {"form_classname": "title", "required": True},
+                    ),
+                    1: (
+                        "wagtail.blocks.ChoiceBlock",
+                        [],
+                        {
+                            "blank": True,
+                            "choices": [
+                                ("", "Select a header size"),
+                                ("h2", "H2"),
+                                ("h3", "H3"),
+                                ("h4", "H4"),
+                            ],
+                            "required": False,
+                        },
+                    ),
+                    2: (
+                        "wagtail.blocks.StructBlock",
+                        [[("heading_text", 0), ("size", 1)]],
+                        {},
+                    ),
+                    3: (
+                        "bakerydemo.base.blocks.CustomRichTextBlock",
+                        (),
+                        {
+                            "description": "A rich text paragraph",
+                            "icon": "pilcrow",
+                            "preview_value": '\n            <h2>Our bread pledge</h2>\n            <p>As a bakery, <b>breads</b> have <i>always</i> been in our hearts.\n            <a href="https://en.wikipedia.org/wiki/Staple_food">Staple foods</a>\n            are essential for society, and – bread is the tastiest of all.\n            We love to transform batters and doughs into baked goods with a firm\n            dry crust and fluffy center.</p>\n            ',
+                            "template": "blocks/paragraph_block.html",
+                        },
+                    ),
+                    4: (
+                        "wagtail.images.blocks.ImageChooserBlock",
+                        (),
+                        {"required": True},
+                    ),
+                    5: ("wagtail.blocks.CharBlock", (), {"required": False}),
+                    6: (
+                        "wagtail.blocks.StructBlock",
+                        [[("image", 4), ("caption", 5), ("attribution", 5)]],
+                        {},
+                    ),
+                    7: ("wagtail.blocks.TextBlock", (), {}),
+                    8: (
+                        "wagtail.blocks.CharBlock",
+                        (),
+                        {"blank": True, "label": "e.g. Mary Berry", "required": False},
+                    ),
+                    9: (
+                        "wagtail.blocks.StructBlock",
+                        [[("text", 7), ("attribute_name", 8)]],
+                        {},
+                    ),
+                    10: (
+                        "bakerydemo.base.blocks.CustomEmbedBlock",
+                        (),
+                        {
+                            "description": "An embedded video or other media",
+                            "help_text": "Insert an embed URL e.g https://www.youtube.com/watch?v=SGJFWirQ3ks",
+                            "icon": "media",
+                            "preview_template": "base/preview/static_embed_block.html",
+                            "preview_value": "https://www.youtube.com/watch?v=mwrGSfiB1Mg",
+                            "template": "blocks/embed_block.html",
+                        },
+                    ),
+                },
+                verbose_name="Page body",
+            ),
+        ),
+    ]

+ 99 - 0
bakerydemo/breads/migrations/0009_alter_breadpage_body.py

@@ -0,0 +1,99 @@
+# Generated by Django 5.1.8 on 2025-06-19 13:28
+
+import wagtail.fields
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("breads", "0008_alter_breadpage_body"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="breadpage",
+            name="body",
+            field=wagtail.fields.StreamField(
+                [
+                    ("heading_block", 2),
+                    ("paragraph_block", 3),
+                    ("image_block", 6),
+                    ("block_quote", 9),
+                    ("embed_block", 10),
+                ],
+                blank=True,
+                block_lookup={
+                    0: (
+                        "wagtail.blocks.CharBlock",
+                        (),
+                        {"form_classname": "title", "required": True},
+                    ),
+                    1: (
+                        "wagtail.blocks.ChoiceBlock",
+                        [],
+                        {
+                            "blank": True,
+                            "choices": [
+                                ("", "Select a header size"),
+                                ("h2", "H2"),
+                                ("h3", "H3"),
+                                ("h4", "H4"),
+                            ],
+                            "required": False,
+                        },
+                    ),
+                    2: (
+                        "wagtail.blocks.StructBlock",
+                        [[("heading_text", 0), ("size", 1)]],
+                        {},
+                    ),
+                    3: (
+                        "bakerydemo.base.blocks.CustomRichTextBlock",
+                        (),
+                        {
+                            "description": "A rich text paragraph",
+                            "icon": "pilcrow",
+                            "preview_value": '\n            <h2>Our bread pledge</h2>\n            <p>As a bakery, <b>breads</b> have <i>always</i> been in our hearts.\n            <a href="https://en.wikipedia.org/wiki/Staple_food">Staple foods</a>\n            are essential for society, and – bread is the tastiest of all.\n            We love to transform batters and doughs into baked goods with a firm\n            dry crust and fluffy center.</p>\n            ',
+                            "template": "blocks/paragraph_block.html",
+                        },
+                    ),
+                    4: (
+                        "wagtail.images.blocks.ImageChooserBlock",
+                        (),
+                        {"required": True},
+                    ),
+                    5: ("wagtail.blocks.CharBlock", (), {"required": False}),
+                    6: (
+                        "wagtail.blocks.StructBlock",
+                        [[("image", 4), ("caption", 5), ("attribution", 5)]],
+                        {},
+                    ),
+                    7: ("wagtail.blocks.TextBlock", (), {}),
+                    8: (
+                        "wagtail.blocks.CharBlock",
+                        (),
+                        {"blank": True, "label": "e.g. Mary Berry", "required": False},
+                    ),
+                    9: (
+                        "wagtail.blocks.StructBlock",
+                        [[("text", 7), ("attribute_name", 8)]],
+                        {},
+                    ),
+                    10: (
+                        "bakerydemo.base.blocks.CustomEmbedBlock",
+                        (),
+                        {
+                            "description": "An embedded video or other media",
+                            "help_text": "Insert an embed URL e.g https://www.youtube.com/watch?v=SGJFWirQ3ks",
+                            "icon": "media",
+                            "preview_template": "base/preview/static_embed_block.html",
+                            "preview_value": "https://www.youtube.com/watch?v=mwrGSfiB1Mg",
+                            "template": "blocks/embed_block.html",
+                        },
+                    ),
+                },
+                verbose_name="Page body",
+            ),
+        ),
+    ]

+ 99 - 0
bakerydemo/locations/migrations/0008_alter_locationpage_body.py

@@ -0,0 +1,99 @@
+# Generated by Django 5.1.8 on 2025-06-19 13:28
+
+import wagtail.fields
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("locations", "0007_alter_locationpage_body"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="locationpage",
+            name="body",
+            field=wagtail.fields.StreamField(
+                [
+                    ("heading_block", 2),
+                    ("paragraph_block", 3),
+                    ("image_block", 6),
+                    ("block_quote", 9),
+                    ("embed_block", 10),
+                ],
+                blank=True,
+                block_lookup={
+                    0: (
+                        "wagtail.blocks.CharBlock",
+                        (),
+                        {"form_classname": "title", "required": True},
+                    ),
+                    1: (
+                        "wagtail.blocks.ChoiceBlock",
+                        [],
+                        {
+                            "blank": True,
+                            "choices": [
+                                ("", "Select a header size"),
+                                ("h2", "H2"),
+                                ("h3", "H3"),
+                                ("h4", "H4"),
+                            ],
+                            "required": False,
+                        },
+                    ),
+                    2: (
+                        "wagtail.blocks.StructBlock",
+                        [[("heading_text", 0), ("size", 1)]],
+                        {},
+                    ),
+                    3: (
+                        "bakerydemo.base.blocks.CustomRichTextBlock",
+                        (),
+                        {
+                            "description": "A rich text paragraph",
+                            "icon": "pilcrow",
+                            "preview_value": '\n            <h2>Our bread pledge</h2>\n            <p>As a bakery, <b>breads</b> have <i>always</i> been in our hearts.\n            <a href="https://en.wikipedia.org/wiki/Staple_food">Staple foods</a>\n            are essential for society, and – bread is the tastiest of all.\n            We love to transform batters and doughs into baked goods with a firm\n            dry crust and fluffy center.</p>\n            ',
+                            "template": "blocks/paragraph_block.html",
+                        },
+                    ),
+                    4: (
+                        "wagtail.images.blocks.ImageChooserBlock",
+                        (),
+                        {"required": True},
+                    ),
+                    5: ("wagtail.blocks.CharBlock", (), {"required": False}),
+                    6: (
+                        "wagtail.blocks.StructBlock",
+                        [[("image", 4), ("caption", 5), ("attribution", 5)]],
+                        {},
+                    ),
+                    7: ("wagtail.blocks.TextBlock", (), {}),
+                    8: (
+                        "wagtail.blocks.CharBlock",
+                        (),
+                        {"blank": True, "label": "e.g. Mary Berry", "required": False},
+                    ),
+                    9: (
+                        "wagtail.blocks.StructBlock",
+                        [[("text", 7), ("attribute_name", 8)]],
+                        {},
+                    ),
+                    10: (
+                        "bakerydemo.base.blocks.CustomEmbedBlock",
+                        (),
+                        {
+                            "description": "An embedded video or other media",
+                            "help_text": "Insert an embed URL e.g https://www.youtube.com/watch?v=SGJFWirQ3ks",
+                            "icon": "media",
+                            "preview_template": "base/preview/static_embed_block.html",
+                            "preview_value": "https://www.youtube.com/watch?v=mwrGSfiB1Mg",
+                            "template": "blocks/embed_block.html",
+                        },
+                    ),
+                },
+                verbose_name="Page body",
+            ),
+        ),
+    ]

+ 5 - 5
bakerydemo/recipes/blocks.py

@@ -4,7 +4,6 @@ from wagtail.blocks import (
     ChoiceBlock,
     FloatBlock,
     ListBlock,
-    RichTextBlock,
     StreamBlock,
     StructBlock,
 )
@@ -15,6 +14,7 @@ from wagtail.images.blocks import ImageBlock
 
 from bakerydemo.base.blocks import (
     BlockQuote,
+    CustomRichTextBlock,
     HeadingBlock,
     get_image_api_representation,
 )
@@ -28,7 +28,7 @@ class CustomImageBlock(ImageBlock):
 
 
 class RecipeStepBlock(StructBlock):
-    text = RichTextBlock(features=["bold", "italic", "link"])
+    text = CustomRichTextBlock(features=["bold", "italic", "link"])
     difficulty = ChoiceBlock(
         widget=forms.RadioSelect,
         choices=[("S", "Small"), ("M", "Medium"), ("L", "Large")],
@@ -46,7 +46,7 @@ class RecipeStreamBlock(StreamBlock):
     """
 
     heading_block = HeadingBlock(group="Content")
-    paragraph_block = RichTextBlock(
+    paragraph_block = CustomRichTextBlock(
         icon="pilcrow", template="blocks/paragraph_block.html", group="Content"
     )
     block_quote = BlockQuote(group="Content")
@@ -67,7 +67,7 @@ class RecipeStreamBlock(StreamBlock):
         [
             ("text", CharBlock()),
             ("numeric", FloatBlock()),
-            ("rich_text", RichTextBlock()),
+            ("rich_text", CustomRichTextBlock()),
             ("image", CustomImageBlock()),
         ],
         group="Content",
@@ -126,7 +126,7 @@ class RecipeStreamBlock(StreamBlock):
     )
 
     ingredients_list = ListBlock(
-        RichTextBlock(features=["bold", "italic", "link"]),
+        CustomRichTextBlock(features=["bold", "italic", "link"]),
         min_num=2,
         max_num=10,
         icon="list-ol",

+ 295 - 0
bakerydemo/recipes/migrations/0004_alter_recipepage_backstory_alter_recipepage_body.py

@@ -0,0 +1,295 @@
+# Generated by Django 5.1.8 on 2025-06-19 13:28
+
+import wagtail.fields
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("recipes", "0003_alter_recipepage_backstory_alter_recipepage_body"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="recipepage",
+            name="backstory",
+            field=wagtail.fields.StreamField(
+                [
+                    ("heading_block", 2),
+                    ("paragraph_block", 3),
+                    ("image_block", 6),
+                    ("block_quote", 9),
+                    ("embed_block", 10),
+                ],
+                blank=True,
+                block_lookup={
+                    0: (
+                        "wagtail.blocks.CharBlock",
+                        (),
+                        {"form_classname": "title", "required": True},
+                    ),
+                    1: (
+                        "wagtail.blocks.ChoiceBlock",
+                        [],
+                        {
+                            "blank": True,
+                            "choices": [
+                                ("", "Select a header size"),
+                                ("h2", "H2"),
+                                ("h3", "H3"),
+                                ("h4", "H4"),
+                            ],
+                            "required": False,
+                        },
+                    ),
+                    2: (
+                        "wagtail.blocks.StructBlock",
+                        [[("heading_text", 0), ("size", 1)]],
+                        {},
+                    ),
+                    3: (
+                        "bakerydemo.base.blocks.CustomRichTextBlock",
+                        (),
+                        {
+                            "description": "A rich text paragraph",
+                            "icon": "pilcrow",
+                            "preview_value": '\n            <h2>Our bread pledge</h2>\n            <p>As a bakery, <b>breads</b> have <i>always</i> been in our hearts.\n            <a href="https://en.wikipedia.org/wiki/Staple_food">Staple foods</a>\n            are essential for society, and – bread is the tastiest of all.\n            We love to transform batters and doughs into baked goods with a firm\n            dry crust and fluffy center.</p>\n            ',
+                            "template": "blocks/paragraph_block.html",
+                        },
+                    ),
+                    4: (
+                        "wagtail.images.blocks.ImageChooserBlock",
+                        (),
+                        {"required": True},
+                    ),
+                    5: ("wagtail.blocks.CharBlock", (), {"required": False}),
+                    6: (
+                        "wagtail.blocks.StructBlock",
+                        [[("image", 4), ("caption", 5), ("attribution", 5)]],
+                        {},
+                    ),
+                    7: ("wagtail.blocks.TextBlock", (), {}),
+                    8: (
+                        "wagtail.blocks.CharBlock",
+                        (),
+                        {"blank": True, "label": "e.g. Mary Berry", "required": False},
+                    ),
+                    9: (
+                        "wagtail.blocks.StructBlock",
+                        [[("text", 7), ("attribute_name", 8)]],
+                        {},
+                    ),
+                    10: (
+                        "bakerydemo.base.blocks.CustomEmbedBlock",
+                        (),
+                        {
+                            "description": "An embedded video or other media",
+                            "help_text": "Insert an embed URL e.g https://www.youtube.com/watch?v=SGJFWirQ3ks",
+                            "icon": "media",
+                            "preview_template": "base/preview/static_embed_block.html",
+                            "preview_value": "https://www.youtube.com/watch?v=mwrGSfiB1Mg",
+                            "template": "blocks/embed_block.html",
+                        },
+                    ),
+                },
+                help_text="Use only a minimum number of headings and large blocks.",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="recipepage",
+            name="body",
+            field=wagtail.fields.StreamField(
+                [
+                    ("heading_block", 2),
+                    ("paragraph_block", 3),
+                    ("block_quote", 6),
+                    ("table_block", 7),
+                    ("typed_table_block", 12),
+                    ("image_block", 13),
+                    ("embed_block", 14),
+                    ("ingredients_list", 16),
+                    ("steps_list", 19),
+                ],
+                blank=True,
+                block_lookup={
+                    0: (
+                        "wagtail.blocks.CharBlock",
+                        (),
+                        {"form_classname": "title", "required": True},
+                    ),
+                    1: (
+                        "wagtail.blocks.ChoiceBlock",
+                        [],
+                        {
+                            "blank": True,
+                            "choices": [
+                                ("", "Select a header size"),
+                                ("h2", "H2"),
+                                ("h3", "H3"),
+                                ("h4", "H4"),
+                            ],
+                            "required": False,
+                        },
+                    ),
+                    2: (
+                        "wagtail.blocks.StructBlock",
+                        [[("heading_text", 0), ("size", 1)]],
+                        {"group": "Content"},
+                    ),
+                    3: (
+                        "bakerydemo.base.blocks.CustomRichTextBlock",
+                        (),
+                        {
+                            "group": "Content",
+                            "icon": "pilcrow",
+                            "template": "blocks/paragraph_block.html",
+                        },
+                    ),
+                    4: ("wagtail.blocks.TextBlock", (), {}),
+                    5: (
+                        "wagtail.blocks.CharBlock",
+                        (),
+                        {"blank": True, "label": "e.g. Mary Berry", "required": False},
+                    ),
+                    6: (
+                        "wagtail.blocks.StructBlock",
+                        [[("text", 4), ("attribute_name", 5)]],
+                        {"group": "Content"},
+                    ),
+                    7: (
+                        "wagtail.contrib.table_block.blocks.TableBlock",
+                        (),
+                        {
+                            "description": "A table of data with plain text cells",
+                            "group": "Content",
+                            "preview_value": {
+                                "data": [
+                                    ["Bread type", "Origin"],
+                                    ["Anpan", "Japan"],
+                                    ["Crumpet", "United Kingdom"],
+                                    ["Roti buaya", "Indonesia"],
+                                ],
+                                "first_row_is_table_header": "True",
+                            },
+                        },
+                    ),
+                    8: ("wagtail.blocks.CharBlock", (), {}),
+                    9: ("wagtail.blocks.FloatBlock", (), {}),
+                    10: ("bakerydemo.base.blocks.CustomRichTextBlock", (), {}),
+                    11: ("wagtail.images.blocks.ImageBlock", [], {}),
+                    12: (
+                        "wagtail.contrib.typed_table_block.blocks.TypedTableBlock",
+                        [
+                            [
+                                ("text", 8),
+                                ("numeric", 9),
+                                ("rich_text", 10),
+                                ("image", 11),
+                            ]
+                        ],
+                        {
+                            "description": "A table of data with cells that can include text, numbers, rich text, and images",
+                            "group": "Content",
+                            "preview_value": {
+                                "caption": "Nutritional information for 100g of bread",
+                                "columns": [
+                                    {"heading": "Nutrient", "type": "rich_text"},
+                                    {"heading": "White bread", "type": "numeric"},
+                                    {"heading": "Brown bread", "type": "numeric"},
+                                    {"heading": "Wholemeal bread", "type": "numeric"},
+                                ],
+                                "rows": [
+                                    {
+                                        "values": [
+                                            '<p><a href="https://en.wikipedia.org/wiki/Protein">Protein</a> <b>(g)</b></p>',
+                                            7.9,
+                                            7.9,
+                                            9.4,
+                                        ]
+                                    },
+                                    {
+                                        "values": [
+                                            '<p><a href="https://en.wikipedia.org/wiki/Carbohydrate">Carbohydrate</a> <b>(g)</b></p>',
+                                            46.1,
+                                            42.1,
+                                            42,
+                                        ]
+                                    },
+                                    {
+                                        "values": [
+                                            '<p><a href="https://en.wikipedia.org/wiki/Sugar">Total sugars</a> <b>(g)</b></p>',
+                                            3.4,
+                                            3.4,
+                                            2.8,
+                                        ]
+                                    },
+                                ],
+                            },
+                        },
+                    ),
+                    13: ("wagtail.images.blocks.ImageBlock", [], {"group": "Media"}),
+                    14: (
+                        "wagtail.embeds.blocks.EmbedBlock",
+                        (),
+                        {
+                            "description": "An embedded video or other media",
+                            "group": "Media",
+                            "help_text": "Insert an embed URL e.g https://www.youtube.com/watch?v=SGJFWirQ3ks",
+                            "icon": "media",
+                            "preview_value": "https://www.youtube.com/watch?v=mwrGSfiB1Mg",
+                            "template": "blocks/embed_block.html",
+                        },
+                    ),
+                    15: (
+                        "bakerydemo.base.blocks.CustomRichTextBlock",
+                        (),
+                        {"features": ["bold", "italic", "link"]},
+                    ),
+                    16: (
+                        "wagtail.blocks.ListBlock",
+                        (15,),
+                        {
+                            "description": "A list of ingredients to use in the recipe with optional bold, italic, and link options",
+                            "group": "Cooking",
+                            "icon": "list-ol",
+                            "max_num": 10,
+                            "min_num": 2,
+                            "preview_value": [
+                                "<p>200g flour</p>",
+                                "<p>1 egg</p>",
+                                "<p>1 cup of sugar</p>",
+                            ],
+                        },
+                    ),
+                    17: (
+                        "wagtail.blocks.ChoiceBlock",
+                        [],
+                        {"choices": [("S", "Small"), ("M", "Medium"), ("L", "Large")]},
+                    ),
+                    18: (
+                        "wagtail.blocks.StructBlock",
+                        [[("text", 15), ("difficulty", 17)]],
+                        {},
+                    ),
+                    19: (
+                        "wagtail.blocks.ListBlock",
+                        (18,),
+                        {
+                            "description": "A list of steps to follow in the recipe, with a difficulty rating for each step",
+                            "group": "Cooking",
+                            "icon": "tasks",
+                            "max_num": 10,
+                            "min_num": 2,
+                            "preview_value": [
+                                {"difficulty": "S", "text": "<p>An easy step</p>"},
+                                {"difficulty": "L", "text": "<p>A difficult step</p>"},
+                                {"difficulty": "M", "text": "<p>A medium step</p>"},
+                            ],
+                        },
+                    ),
+                },
+                help_text="The recipe's step-by-step instructions and any other relevant information.",
+            ),
+        ),
+    ]

+ 2 - 1
bakerydemo/recipes/models.py

@@ -13,6 +13,7 @@ from wagtail.search import index
 
 from bakerydemo.base.blocks import BaseStreamBlock
 from bakerydemo.headless import CustomHeadlessMixin
+from bakerydemo.serializers import RichTextSerializer
 
 from .blocks import RecipeStreamBlock
 
@@ -119,7 +120,7 @@ class RecipePage(CustomHeadlessMixin, Page):
         APIField("subtitle"),
         APIField("introduction"),
         APIField("backstory"),
-        APIField("recipe_headline"),
+        APIField("recipe_headline", serializer=RichTextSerializer()),
         APIField("body"),
         APIField("recipe_person_relationship"),
     ]

+ 7 - 0
bakerydemo/serializers.py

@@ -0,0 +1,7 @@
+from rest_framework.fields import CharField
+from wagtail.rich_text import expand_db_html
+
+
+class RichTextSerializer(CharField):
+    def to_representation(self, instance):
+        return expand_db_html(super().to_representation(instance))