diff --git a/.gitignore b/.gitignore
index c89290c9..e04e06b4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,4 @@ apps/guide/settings/local.py
/static/
.coverage
.ruff_cache
+.idea/
diff --git a/apps/core/blocks.py b/apps/core/blocks.py
index eae2f08e..7faa6d84 100644
--- a/apps/core/blocks.py
+++ b/apps/core/blocks.py
@@ -2,12 +2,52 @@
from wagtail import blocks
from wagtail.blocks import RichTextBlock
+WAGTAIL_VERSIONS = [
+ "4.1",
+ "4.2",
+ "5.0",
+ "5.1",
+ "5.2",
+ "6.0",
+ "6.1",
+ "6.2",
+ "6.3",
+ "6.4",
+ "7.0",
+ "7.1",
+ "7.2",
+ "7.3",
+ "7.4",
+ "8.0",
+]
+
class TextBlock(RichTextBlock):
class Meta:
template = "core/blocks/text.html"
+class TextBlockVersioned(blocks.StructBlock):
+ content = RichTextBlock(features=["bold", "italic", "link"])
+ version = blocks.ChoiceBlock(choices=[(v, v) for v in WAGTAIL_VERSIONS])
+ change_type = blocks.ChoiceBlock(
+ choices=[
+ ("added", _("Added")),
+ ("changed", _("Changed")),
+ ("removed", _("Removed")),
+ ]
+ )
+
+ class Meta:
+ template = "core/blocks/text_versioned.html"
+ icon = "pilcrow"
+ label = _("Text (versioned)")
+ form_layout = blocks.BlockGroup(
+ children=["content"],
+ settings=["version", "change_type"],
+ )
+
+
class SectionStructValue(blocks.StructValue):
def icon(self):
return f"core/svg/{self.get('section')}.svg"
@@ -62,5 +102,40 @@ class Meta:
value_class = AlertStructValue
-CONTENT_BLOCKS = [("text", TextBlock()), ("alert", AlertBlock())]
+class VersionNoteStructValue(blocks.StructValue):
+ def icon(self):
+ return f"core/svg/{self.get('change_type')}.svg"
+
+
+class VersionNoteBlock(blocks.StructBlock):
+ version = blocks.ChoiceBlock(
+ choices=[(v, v) for v in WAGTAIL_VERSIONS],
+ label=_("Version"),
+ )
+ change_type = blocks.ChoiceBlock(
+ choices=[
+ ("added", _("Added")),
+ ("changed", _("Changed")),
+ ("removed", _("Removed")),
+ ],
+ label=_("Type of change"),
+ )
+ content = RichTextBlock(
+ features=["bold", "italic", "link"],
+ label=_("Content"),
+ )
+
+ class Meta:
+ template = "core/blocks/version_note.html"
+ icon = "tag"
+ label = _("Version note")
+ value_class = VersionNoteStructValue
+
+
+CONTENT_BLOCKS = [
+ ("text", TextBlock()),
+ ("text_versioned", TextBlockVersioned()),
+ ("alert", AlertBlock()),
+ ("version_note", VersionNoteBlock()),
+]
HOME_BLOCKS = [("section_grid", SectionGridBlock())]
diff --git a/apps/core/migrations/0012_alter_contentpage_body.py b/apps/core/migrations/0012_alter_contentpage_body.py
new file mode 100644
index 00000000..d8eaaef4
--- /dev/null
+++ b/apps/core/migrations/0012_alter_contentpage_body.py
@@ -0,0 +1,19 @@
+# Generated by Django 6.0.6 on 2026-06-21 12:17
+
+import wagtail.fields
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0011_alter_footercontent_locale_alter_footeritem_locale'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='contentpage',
+ name='body',
+ field=wagtail.fields.StreamField([('text', 0), ('text_versioned', 4), ('alert', 6), ('version_note', 10)], block_lookup={0: ('apps.core.blocks.TextBlock', (), {}), 1: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link']}), 2: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('4.1', '4.1'), ('4.2', '4.2'), ('5.0', '5.0'), ('5.1', '5.1'), ('5.2', '5.2'), ('6.0', '6.0'), ('6.1', '6.1'), ('6.2', '6.2'), ('6.3', '6.3'), ('6.4', '6.4'), ('7.0', '7.0'), ('7.1', '7.1'), ('7.2', '7.2'), ('7.3', '7.3'), ('7.4', '7.4'), ('8.0', '8.0')]}), 3: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('added', 'Added'), ('changed', 'Changed'), ('removed', 'Removed')]}), 4: ('wagtail.blocks.StructBlock', [[('content', 1), ('version', 2), ('change_type', 3)]], {}), 5: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('Warning', 'Warning'), ('Note', 'Note')]}), 6: ('wagtail.blocks.StructBlock', [[('alert_type', 5), ('alert_body', 1)]], {}), 7: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('4.1', '4.1'), ('4.2', '4.2'), ('5.0', '5.0'), ('5.1', '5.1'), ('5.2', '5.2'), ('6.0', '6.0'), ('6.1', '6.1'), ('6.2', '6.2'), ('6.3', '6.3'), ('6.4', '6.4'), ('7.0', '7.0'), ('7.1', '7.1'), ('7.2', '7.2'), ('7.3', '7.3'), ('7.4', '7.4'), ('8.0', '8.0')], 'label': 'Version'}), 8: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('added', 'Added'), ('changed', 'Changed'), ('removed', 'Removed')], 'label': 'Type of change'}), 9: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'label': 'Content'}), 10: ('wagtail.blocks.StructBlock', [[('version', 7), ('change_type', 8), ('content', 9)]], {})}),
+ ),
+ ]
diff --git a/apps/core/templates/core/blocks/text_versioned.html b/apps/core/templates/core/blocks/text_versioned.html
new file mode 100644
index 00000000..cf35dbbb
--- /dev/null
+++ b/apps/core/templates/core/blocks/text_versioned.html
@@ -0,0 +1,8 @@
+{% load wagtailcore_tags %}
+
+{% if value.version %}
+
+ {{ value.change_type|title }} in Wagtail {{ value.version }}
+
+{% endif %}
+{{ value.content|richtext }}
diff --git a/apps/core/templates/core/blocks/version_note.html b/apps/core/templates/core/blocks/version_note.html
new file mode 100644
index 00000000..b65d5c3a
--- /dev/null
+++ b/apps/core/templates/core/blocks/version_note.html
@@ -0,0 +1,17 @@
+{% load wagtailcore_tags i18n %}
+
+
+
+
+ {{ value.content|richtext }}
+
+
diff --git a/apps/core/templates/core/svg/added.svg b/apps/core/templates/core/svg/added.svg
new file mode 100644
index 00000000..66bf4dff
--- /dev/null
+++ b/apps/core/templates/core/svg/added.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/core/templates/core/svg/changed.svg b/apps/core/templates/core/svg/changed.svg
new file mode 100644
index 00000000..e6ede22a
--- /dev/null
+++ b/apps/core/templates/core/svg/changed.svg
@@ -0,0 +1,4 @@
+
diff --git a/apps/core/templates/core/svg/removed.svg b/apps/core/templates/core/svg/removed.svg
new file mode 100644
index 00000000..637fe162
--- /dev/null
+++ b/apps/core/templates/core/svg/removed.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/frontend/static_src/scss/components/version-badge.scss b/apps/frontend/static_src/scss/components/version-badge.scss
new file mode 100644
index 00000000..1f4e168f
--- /dev/null
+++ b/apps/frontend/static_src/scss/components/version-badge.scss
@@ -0,0 +1,34 @@
+.version-badge {
+ display: inline-flex;
+ align-items: center;
+ margin-bottom: ($gutter * 0.5);
+ padding: 2px 10px;
+ border-radius: $border-radius--lg;
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 1.5;
+
+ &--added {
+ background-color: light-dark(
+ $color--extra-light-teal,
+ rgba($color--light-teal, 0.15)
+ );
+ color: light-dark($color--teal, $color--light-teal);
+ }
+
+ &--changed {
+ background-color: light-dark(
+ $color--extra-light-blue,
+ rgba($color--light-blue, 0.15)
+ );
+ color: light-dark($color--blue, $color--light-blue);
+ }
+
+ &--removed {
+ background-color: light-dark(
+ rgba($color--grey, 0.15),
+ rgba($color--light-grey, 0.1)
+ );
+ color: light-dark($color--grey, $color--light-grey);
+ }
+}
diff --git a/apps/frontend/static_src/scss/components/version-note.scss b/apps/frontend/static_src/scss/components/version-note.scss
new file mode 100644
index 00000000..52f5511d
--- /dev/null
+++ b/apps/frontend/static_src/scss/components/version-note.scss
@@ -0,0 +1,82 @@
+.version-note {
+ $root: &;
+ box-shadow: 2px 2px 8px 0 rgba($color--black, 0.3);
+ border-radius: $border-radius--m;
+ margin-bottom: ($gutter * 3);
+ overflow: hidden;
+
+ &__header {
+ display: flex;
+ align-items: center;
+ padding: ($gutter * 0.75) $gutter;
+ }
+
+ &__label {
+ color: $color--white;
+ font-weight: 700;
+ }
+
+ &__body {
+ padding: $gutter;
+ }
+
+ &__icon {
+ display: flex;
+ align-items: center;
+ margin-inline-end: ($gutter * 0.5);
+ height: 20px;
+
+ svg {
+ height: 100%;
+ }
+ }
+
+ &--added {
+ #{$root}__header {
+ background-color: $color--primary;
+ }
+
+ #{$root}__body {
+ background-color: light-dark(
+ $color--extra-light-teal,
+ $color--black
+ );
+ }
+ }
+
+ &--changed {
+ #{$root}__header {
+ background-color: $color--blue;
+ }
+
+ #{$root}__body {
+ background-color: light-dark(
+ $color--extra-light-blue,
+ $color--black
+ );
+ }
+ }
+
+ &--removed {
+ #{$root}__header {
+ background-color: $color--grey;
+ }
+
+ #{$root}__body {
+ background-color: light-dark(
+ rgba($color--grey, 0.15),
+ $color--black
+ );
+ }
+ }
+
+ // Extra specificity to override default StreamField values
+ .streamfield & {
+ #{$root}__label {
+ @include rem-font-size($base-font-size);
+ /* stylelint-disable-next-line declaration-no-important */
+ margin: 0 !important;
+ line-height: 1;
+ }
+ }
+}
diff --git a/apps/frontend/static_src/scss/main.scss b/apps/frontend/static_src/scss/main.scss
index 14b21fe5..7a6443a6 100644
--- a/apps/frontend/static_src/scss/main.scss
+++ b/apps/frontend/static_src/scss/main.scss
@@ -15,6 +15,8 @@
// Custom component styles
@import './components/alert';
+@import './components/version-note';
+@import './components/version-badge';
@import './components/app';
@import './components/copy-button';
@import './components/autocomplete';
diff --git a/apps/llms_txt/jinja2/llms_txt/page.md.jinja b/apps/llms_txt/jinja2/llms_txt/page.md.jinja
index 066ad3c0..7a4a7bb0 100644
--- a/apps/llms_txt/jinja2/llms_txt/page.md.jinja
+++ b/apps/llms_txt/jinja2/llms_txt/page.md.jinja
@@ -10,8 +10,12 @@ Page URL: {{ page.full_url }}
{%- for block in page.body %}
{%- if block.block_type == 'text' %}
{{ block.value|richtext_markdown }}
+{%- elif block.block_type == 'text_versioned' %}
+{{ block.value.content|richtext_markdown }}
{%- elif block.block_type == 'alert' %}
{{block.value.alert_type}}: {{ block.value.alert_body|richtext_markdown }}
+{%- elif block.block_type == 'version_note' %}
+{{ block.value.change_type|capitalize }} in Wagtail {{ block.value.version }}: {{ block.value.content|richtext_markdown }}
{%- endif %}
{%- endfor %}