Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a5fa3c4
add an AnonymTopAndBottomPage to the project
dchukhin Dec 19, 2025
7beb387
add counter to section templates
dchukhin Dec 19, 2025
eafe2a1
only show icon element if icon is set
dchukhin Dec 19, 2025
d93d6b8
add CSS class to sections with 'top glow' theme
dchukhin Dec 20, 2025
5b78ce6
add table block to SectionBlocks
dchukhin Dec 20, 2025
7a1bf18
Merge branch 'WT-514-anonym-homepage-cms' into anonym-index-child-page
dchukhin Dec 20, 2025
5cdfaab
make sure first section heading is an h1 element, and others are h2 e…
dchukhin Dec 20, 2025
0c4e399
add an AnonymContentSubPage to the project
dchukhin Dec 20, 2025
12b618b
add toggleable items component to AnonymContentSubPage
dchukhin Dec 22, 2025
4ed3e28
Merge branch 'WT-514-anonym-homepage-cms' into anonym-index-child-page
dchukhin Dec 22, 2025
8503cb0
update migration, since CardsListBlock.cards is now a StreamBlock, to…
dchukhin Dec 22, 2025
9412913
add logo cards, to allow creating case study cards on AnonymContentSu…
dchukhin Dec 22, 2025
25b1fab
Merge branch 'anonym-index-child-page' into anonym-index-content-subpage
dchukhin Dec 22, 2025
c754b6d
update toggle to handle dark and light icons automatically
dchukhin Dec 22, 2025
b88e8c3
add a configurable navigation to anonym pages
dchukhin Dec 22, 2025
22b27e0
make sure migration has a license
dchukhin Dec 22, 2025
467bd13
Merge branch 'anonym-index-content-subpage' into anonym-navbar
dchukhin Dec 22, 2025
551030e
Merge branch 'WT-514-anonym-homepage-cms' into anonym-index-child-page
dchukhin Dec 22, 2025
dc564b2
update body class to be anonym-specific
dchukhin Dec 22, 2025
e1346c5
Merge branch 'anonym-index-child-page' into anonym-index-content-subpage
dchukhin Dec 22, 2025
fc72654
update body class to be anonym-specific
dchukhin Dec 22, 2025
ebc0e53
Merge branch 'WT-514-anonym-homepage-cms' into anonym-index-child-page
dchukhin Dec 23, 2025
110f9df
make sure AnonymTopAndBottomPage can be added in production
dchukhin Dec 23, 2025
51c8b61
make sure AnonymTopAndBottomPage is included in db export script
dchukhin Dec 23, 2025
66b2458
Merge branch 'anonym-index-child-page' into anonym-index-content-subpage
dchukhin Dec 23, 2025
3592fee
make sure AnonymContentSubPage can be added in production
dchukhin Dec 23, 2025
8bd545d
make sure AnonymContentSubPage is included in db export script
dchukhin Dec 23, 2025
a41307c
Merge branch 'anonym-index-content-subpage' into anonym-navbar
dchukhin Dec 23, 2025
9445663
Merge branch 'WT-514-anonym-homepage-cms' into anonym-index-child-page
dchukhin Dec 23, 2025
b623f94
make instances of AnonymTopAndBottomPage more easily identifiable by …
dchukhin Dec 23, 2025
1241373
Merge branch 'anonym-index-child-page' into anonym-index-content-subpage
dchukhin Dec 23, 2025
2cb9951
make instances of AnonymContentSubPage more easily identifiable by se…
dchukhin Dec 23, 2025
e6a380c
Merge branch 'anonym-index-content-subpage' into anonym-navbar
dchukhin Dec 23, 2025
15e8e1b
Merge branch 'WT-514-anonym-homepage-cms' into anonym-navbar
dchukhin Jan 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion bedrock/mozorg/blocks/anonym.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,11 @@ def has_top_glow_theme(self) -> bool:


class SectionBlockSettings(blocks.StructBlock):
anchor_id = blocks.CharBlock(
required=False,
max_length=100,
help_text="Optional: Add an ID to make this section linkable from navigation (e.g., 'overview', 'features')",
)
theme = ThumbnailChoiceBlock(
choices=(
(SECTION_THEME_INDEX, "Index"),
Expand All @@ -497,7 +502,7 @@ class Meta:
icon = "cog"
collapsed = True
label = "Settings"
label_format = "Theme: {theme}"
label_format = "ID: {anchor_id} - Theme: {theme}"
form_classname = "compact-form struct-block"
value_class = SectionBlockSettingsValue

Expand Down Expand Up @@ -561,7 +566,23 @@ class Meta:
label_format = "Toggle Item - {toggle_text}"


class ToggleableItemsBlockSettings(blocks.StructBlock):
anchor_id = blocks.CharBlock(
required=False,
max_length=100,
help_text="Optional: Add an ID to make this section linkable from navigation",
)

class Meta:
icon = "cog"
collapsed = True
label = "Settings"
label_format = "ID: {anchor_id}"
form_classname = "compact-form struct-block"


class ToggleableItemsBlock(blocks.StructBlock):
settings = ToggleableItemsBlockSettings()
toggle_items = blocks.StreamBlock(
[
("toggle_items", ToggleableItemBlock()),
Expand All @@ -574,7 +595,23 @@ class Meta:
label = "Toggle Items"


class CallToActionBlockSettings(blocks.StructBlock):
anchor_id = blocks.CharBlock(
required=False,
max_length=100,
help_text="Optional: Add an ID to make this section linkable from navigation",
)

class Meta:
icon = "cog"
collapsed = True
label = "Settings"
label_format = "ID: {anchor_id}"
form_classname = "compact-form struct-block"


class CallToActionBlock(blocks.StructBlock):
settings = CallToActionBlockSettings()
heading = blocks.RichTextBlock(
features=BASIC_TEXT_FEATURES,
help_text="Use <strong>bold</strong> to make parts of this text black.",
Expand Down
2,134 changes: 2,134 additions & 0 deletions bedrock/mozorg/migrations/0027_anonymindexpage_navigation_and_more.py

Large diffs are not rendered by default.

131 changes: 93 additions & 38 deletions bedrock/mozorg/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,65 @@
from bedrock.mozorg.blocks.navigation import NavigationLinkBlock


class SubNavigationMixin:
"""
Mixin for pages with navigation that validates anchor IDs.

Pages using this mixin should:
1. Have a navigation StreamField (defaults to 'sub_navigation', configurable via navigation_field_name)
2. Implement get_available_sections() to return a list of all anchor IDs
"""

navigation_field_name = "sub_navigation"

def get_available_sections(self):
"""
Return a list of all anchor IDs available in the page's content.
Subclasses should override this method to collect anchor IDs from their specific content fields.
"""
raise NotImplementedError("Subclasses must implement get_available_sections()")

def clean(self):
"""
Validate that:
1. Anchor IDs in content blocks are unique
2. Navigation links reference valid sections
"""
super().clean()

# Get available section anchors
available_sections = self.get_available_sections()

# Check for duplicate anchor IDs
if len(available_sections) != len(set(available_sections)):
# Find duplicates
seen = set()
duplicates = set()
for anchor_id in available_sections:
if anchor_id in seen:
duplicates.add(anchor_id)
seen.add(anchor_id)

raise ValidationError(f"Duplicate anchor ID(s) found: {', '.join(sorted(duplicates))}. Each anchor ID must be unique.")

# Validate navigation links reference valid sections
navigation_field = getattr(self, self.navigation_field_name, None)
if navigation_field:
available_sections_set = set(available_sections)
invalid_anchors = []
for nav_item in navigation_field:
link = nav_item.value.get("link", {})
if link.get("link_to") == "anchor":
anchor = link.get("anchor", "")
if anchor and anchor not in available_sections_set:
invalid_anchors.append(anchor)

if invalid_anchors:
available_text = ", ".join(available_sections) if available_sections else "None"
invalid_text = ", ".join(invalid_anchors)
raise ValidationError(f"Navigation links reference unknown section(s): '{invalid_text}'. Available sections: {available_text}")


def process_md_file(file_path):
try:
# Parse the Markdown file
Expand Down Expand Up @@ -200,8 +259,9 @@ class LeadershipPage(AbstractBedrockCMSPage):
template = "mozorg/cms/about/leadership.html"


class AdvertisingIndexPage(AbstractBedrockCMSPage):
class AdvertisingIndexPage(SubNavigationMixin, AbstractBedrockCMSPage):
subpage_types = ["AdvertisingTwoColumnSubpage", "ContentSubpage"]
navigation_field_name = "sub_navigation"

sub_navigation = StreamField(
[("link", NavigationLinkBlock())],
Expand Down Expand Up @@ -253,20 +313,17 @@ class AdvertisingIndexPage(AbstractBedrockCMSPage):

template = "mozorg/cms/advertising/advertising_index_page.html"

def clean(self):
"""
Validate that:
1. Anchor IDs in content blocks are unique
2. Sub-navigation links reference valid sections
"""
super().clean()

# Get available section anchors and check for duplicates
def get_available_sections(self):
"""Collect all anchor IDs from hero and sections."""
available_sections = []

# Check hero blocks
for block in self.hero:
anchor_id = block.value.get("anchor_id")
if anchor_id:
available_sections.append(anchor_id)

# Check section blocks
for block in self.sections:
# SectionBlock has anchor_id nested under settings
settings = block.value.get("settings", {})
Expand All @@ -286,33 +343,7 @@ def clean(self):
if section_block_anchor_id:
available_sections.append(section_block_anchor_id)

# Check for duplicate anchor IDs
if len(available_sections) != len(set(available_sections)):
# Find duplicates
seen = set()
duplicates = set()
for anchor_id in available_sections:
if anchor_id in seen:
duplicates.add(anchor_id)
seen.add(anchor_id)

raise ValidationError(f"Duplicate anchor ID(s) found: {', '.join(sorted(duplicates))}. Each anchor ID must be unique.")

# Validate navigation links reference valid sections
if self.sub_navigation:
available_sections_set = set(available_sections)
invalid_anchors = []
for nav_item in self.sub_navigation:
link = nav_item.value.get("link", {})
if link.get("link_to") == "anchor":
anchor = link.get("anchor", "")
if anchor and anchor not in available_sections_set:
invalid_anchors.append(anchor)

if invalid_anchors:
available_text = ", ".join(available_sections) if available_sections else "None"
invalid_text = ", ".join(invalid_anchors)
raise ValidationError(f"Navigation links reference unknown section(s): '{invalid_text}'. Available sections: {available_text}")
return available_sections


class AdvertisingTwoColumnSubpage(AbstractBedrockCMSPage):
Expand Down Expand Up @@ -372,9 +403,17 @@ class ContentSubpage(AbstractBedrockCMSPage):
template = "mozorg/cms/advertising/content_subpage.html"


class AnonymIndexPage(AbstractBedrockCMSPage):
class AnonymIndexPage(SubNavigationMixin, AbstractBedrockCMSPage):
subpage_types = ["AnonymTopAndBottomPage", "AnonymContentSubPage"]
navigation_field_name = "navigation"

navigation = StreamField(
[("link", NavigationLinkBlock())],
blank=True,
null=True,
use_json_field=True,
help_text="Configure the navigation menu items.",
)
content = StreamField(
[
("section", AnonymSectionBlock()),
Expand All @@ -388,9 +427,25 @@ class AnonymIndexPage(AbstractBedrockCMSPage):
content_panels = AbstractBedrockCMSPage.content_panels + [
FieldPanel("content"),
]
settings_panels = AbstractBedrockCMSPage.settings_panels + [
FieldPanel("navigation"),
]

template = "mozorg/cms/anonym/anonym_index_page.html"

def get_available_sections(self):
"""Collect all anchor IDs from content blocks."""
available_sections = []

for block in self.content:
# Anchor IDs are nested under settings
settings = block.value.get("settings", {})
anchor_id = settings.get("anchor_id") if settings else None
if anchor_id:
available_sections.append(anchor_id)

return available_sections

class Meta:
verbose_name = "Anonym Index Page"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
{% endblock %}

{% block site_header %}
{% with has_subnav=true %}
{% include 'includes/protocol/navigation/navigation.html' %}
{% with anonym_index_page=page.get_parent() %}
{% include 'mozorg/cms/anonym/includes/navigation.html' %}
{% endwith %}
{% endblock %}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
{% endblock %}

{% block site_header %}
{% with has_subnav=true %}
{% include 'includes/protocol/navigation/navigation.html' %}
{% with anonym_index_page=page %}
{% include 'mozorg/cms/anonym/includes/navigation.html' %}
{% endwith %}
{% endblock %}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
{% endblock %}

{% block site_header %}
{% with has_subnav=true %}
{% include 'includes/protocol/navigation/navigation.html' %}
{% with anonym_index_page=page.get_parent() %}
{% include 'mozorg/cms/anonym/includes/navigation.html' %}
{% endwith %}
{% endblock %}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{#
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/.
#}

<nav class="m24-navigation-refresh {% if not has_subnav %}enable-main-nav-sticky m24-mzp-is-sticky{% endif %}" aria-label="{{ ftl('navigation-refresh-landmark-label') }}">
<div class="m24-c-navigation-l-content">
<div class="m24-c-navigation-container">
<button class="m24-c-navigation-menu-button" type="button" aria-controls="m24-c-navigation-items" data-testid="m24-navigation-menu-button">
{{ ftl('ui-menu') }}
</button>
<a
class="m24-c-navigation-logo-link"
href="{{ anonym_index_page.url }}"
data-link-text="Mozilla Anonym"
data-link-position="nav"
aria-label="Mozilla Anonym"
>
<svg width="112" height="24" viewBox="0 0 112 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M97.0747 10.2628C97.0747 9.23975 96.8707 8.48784 96.4682 8.00706C96.0824 7.50672 95.4759 7.25515 94.6457 7.25515C93.8155 7.25515 93.2285 7.50672 92.8232 8.00706C92.4207 8.5074 92.2167 9.25931 92.2167 10.2628V19.951H88.3397V7.3446H86.6626V4.51026H91.6968V7.54585H92.6192C92.8735 6.40821 93.304 5.56965 93.9217 5.03017C94.559 4.4907 95.4172 4.21956 96.4961 4.21956C97.5751 4.21956 98.4444 4.49908 99.1012 5.05812C99.7553 5.5976 100.208 6.42777 100.46 7.54585H101.329C101.581 6.40821 102.022 5.56965 102.66 5.03017C103.297 4.4907 104.155 4.21956 105.234 4.21956C106.701 4.21956 107.808 4.69195 108.56 5.63673C109.332 6.56194 109.715 7.92041 109.715 9.71493V17.1194H111.392V19.9538H105.81V10.2656C105.81 9.26211 105.606 8.5102 105.203 8.00986C104.817 7.50952 104.219 7.25795 103.409 7.25795C102.598 7.25795 101.964 7.50952 101.558 8.00986C101.175 8.5102 100.98 9.26211 100.98 10.2656V19.9538H97.0775V10.2656L97.0747 10.2628Z" fill="#161616"/>
<path d="M72.1221 20.9363H76.8655L77.3547 19.1446H74.0032L70.2409 4.51172H74.3191L77.2093 16.3382H78.1345L81.0555 4.51172H84.9297L80.6223 21.3137C80.3903 22.1606 80.0632 22.7784 79.6384 23.1641C79.2358 23.5694 78.6488 23.7707 77.8746 23.7707H72.1221V20.9363Z" fill="#161616"/>
<path d="M54.6548 7.3446H52.9777V4.51026H58.0119V7.54585H58.9343C59.2054 6.40821 59.6778 5.56965 60.3542 5.03017C61.0279 4.4907 61.9223 4.21956 63.0432 4.21956C64.5666 4.21956 65.7238 4.69195 66.5121 5.63673C67.3227 6.56194 67.728 7.92041 67.728 9.71493V17.1194H69.4051V19.9538H63.8231V10.4696C63.8231 9.35155 63.6107 8.54095 63.1858 8.0406C62.7637 7.5207 62.0984 7.26074 61.19 7.26074C60.2816 7.26074 59.6107 7.5207 59.1635 8.0406C58.7414 8.54095 58.529 9.35155 58.529 10.4696V19.9538H54.652V7.3446H54.6548Z" fill="#161616"/>
<path d="M44.0305 20.2418C42.4679 20.2418 41.129 19.9343 40.011 19.3165C38.9125 18.6988 38.0739 17.7932 37.4953 16.5996C36.9167 15.4033 36.626 13.9498 36.626 12.2335C36.626 10.5173 36.9167 9.06096 37.4953 7.86741C38.0739 6.67106 38.9125 5.76542 40.011 5.15047C41.129 4.53273 42.4679 4.22526 44.0305 4.22526C45.593 4.22526 46.8927 4.53273 47.9913 5.15047C49.1093 5.76821 49.9591 6.67386 50.5349 7.86741C51.1135 9.06375 51.4042 10.5173 51.4042 12.2335C51.4042 13.9498 51.1135 15.4061 50.5349 16.5996C49.9591 17.796 49.1093 18.7016 47.9913 19.3165C46.8927 19.9343 45.5706 20.2418 44.0305 20.2418ZM44.0305 17.2034C45.1485 17.2034 45.9675 16.8959 46.4874 16.2782C47.0074 15.6409 47.2701 14.6486 47.2701 13.2985V11.1574C47.2701 9.80728 47.0074 8.82337 46.4874 8.20842C45.9675 7.57112 45.1485 7.25526 44.0305 7.25526C42.9124 7.25526 42.0627 7.57391 41.5427 8.20842C41.0228 8.82616 40.7601 9.80728 40.7601 11.1574V13.2985C40.7601 14.6486 41.0228 15.6409 41.5427 16.2782C42.0627 16.8959 42.8928 17.2034 44.0305 17.2034Z" fill="#161616"/>
<path d="M20.4662 7.3446H18.7891V4.51026H23.8204V7.54585H24.7456C25.0168 6.40821 25.4892 5.56965 26.1628 5.03017C26.8392 4.4907 27.7337 4.21956 28.8546 4.21956C30.3752 4.21956 31.5352 4.69195 32.3234 5.63673C33.134 6.56194 33.5393 7.92041 33.5393 9.71493V17.1194H35.2165V19.9538H29.6344V10.4696C29.6344 9.35155 29.4192 8.54095 28.9971 8.0406C28.5723 7.5207 27.907 7.26074 27.0014 7.26074C26.0957 7.26074 25.4193 7.5207 24.9748 8.0406C24.5528 8.54095 24.3375 9.35155 24.3375 10.4696V19.9538H20.4634V7.3446H20.4662Z" fill="#161616"/>
<path d="M4.97825 0H12.2374L17.2101 19.9549H13.0452L8.96701 3.35704H8.06975L3.99155 19.9549H0L4.97825 0ZM3.53034 12.6371H13.3639L13.4226 15.4435H3.44649L3.53314 12.6371H3.53034Z" fill="#161616"/>
</svg>
</a>

<div class="m24-c-navigation-items">
<div class="m24-c-navigation-menu">
<div class="m24-c-menu m24-mzp-is-basic">
<ul class="m24-c-menu-category-list">
{% for link_block in anonym_index_page.navigation %}
{% with link_block=link_block.value %}
<li class="c-sub-navigation-item">
<a
href="{{ link_block.get_resolved_url(anchor_page_url=anonym_index_page.url) }}"
data-link-position="nav"
data-link-text="{{ link_block.link_text }}"
{% if link_block.has_button_appearance %}class="mzan-c-contact-button"{% endif %}
{% if link_block.link.new_window %}target="_blank" rel="noopener"{% endif %}
>
{{ link_block.link_text }}
</a>
</li>
{% endwith %}
{% endfor %}
</ul>
</div>
</div><!-- close .m24-c-navigation-menu -->
</div><!-- close .m24-c-navigation-items -->
</div><!-- close .m24-c-navigation-container -->
</div><!-- close .m24-c-navigation-l-content -->
</nav>
1 change: 1 addition & 0 deletions media/css/mozorg/anonym.scss
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ $image-path: '/media/protocol/img';
.icon-highlight { --icon-src: url('/media/protocol/img/icons/highlight.svg'); }
.icon-history { --icon-src: url('/media/protocol/img/icons/history.svg'); }
.icon-home { --icon-src: url('/media/protocol/img/icons/home.svg'); }
.icon-horizontal-lines { --icon-src: url('/media/protocol/img/icons/horizontal-lines.svg'); }
.icon-hubs { --icon-src: url('/media/protocol/img/icons/hubs.svg'); }
.icon-identity-notification { --icon-src: url('/media/protocol/img/icons/identity-notification.svg'); }
.icon-identity { --icon-src: url('/media/protocol/img/icons/identity.svg'); }
Expand Down
6 changes: 6 additions & 0 deletions media/img/icons/horizontal-lines.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.