feat: replace hardcoded navigation with CMS-managed models
Replace static nav/footer links with Wagtail-managed NavigationMenuItem and SocialMediaLink orderables on SiteSettings. Unpublished pages are automatically excluded from rendering, fixing the dead-link problem. - Extend SiteSettings with site_name, tagline, footer_description, copyright_text branding fields - Add NavigationMenuItem orderable (link_page/link_url, show_in_header, show_in_footer, sort_order) with automatic live-page filtering - Add SocialMediaLink orderable with platform icon templates - New template tags: get_nav_items, get_social_links - Update nav.html and footer.html to render from CMS data - Data migration seeds existing hardcoded values for zero-change deploy - Update seed_e2e_content command for test/dev environments - 18 new tests covering models, template tags, and rendered output Closes #32 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,10 +1,24 @@
|
||||
from django.db import models
|
||||
from django.db.models import SET_NULL
|
||||
from modelcluster.fields import ParentalKey
|
||||
from modelcluster.models import ClusterableModel
|
||||
from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel
|
||||
from wagtail.contrib.settings.models import BaseSiteSetting, register_setting
|
||||
from wagtail.models import Orderable
|
||||
|
||||
SOCIAL_ICON_CHOICES = [
|
||||
("twitter", "Twitter / X"),
|
||||
("github", "GitHub"),
|
||||
("rss", "RSS Feed"),
|
||||
("linkedin", "LinkedIn"),
|
||||
("youtube", "YouTube"),
|
||||
("mastodon", "Mastodon"),
|
||||
("bluesky", "Bluesky"),
|
||||
]
|
||||
|
||||
|
||||
@register_setting
|
||||
class SiteSettings(BaseSiteSetting):
|
||||
class SiteSettings(ClusterableModel, BaseSiteSetting):
|
||||
default_og_image = models.ForeignKey(
|
||||
"wagtailimages.Image",
|
||||
null=True,
|
||||
@@ -19,3 +33,140 @@ class SiteSettings(BaseSiteSetting):
|
||||
on_delete=SET_NULL,
|
||||
related_name="+",
|
||||
)
|
||||
|
||||
# Branding
|
||||
site_name = models.CharField(max_length=100, default="NO HYPE AI")
|
||||
tagline = models.CharField(
|
||||
max_length=200,
|
||||
default="Honest AI tool reviews for developers.",
|
||||
)
|
||||
footer_description = models.TextField(
|
||||
default="In-depth reviews and benchmarks of the latest AI coding tools.\nHonest analysis for developers.",
|
||||
blank=True,
|
||||
)
|
||||
copyright_text = models.CharField(
|
||||
max_length=200,
|
||||
default="No Hype AI. All rights reserved.",
|
||||
)
|
||||
|
||||
panels = [
|
||||
MultiFieldPanel(
|
||||
[
|
||||
FieldPanel("site_name"),
|
||||
FieldPanel("tagline"),
|
||||
FieldPanel("footer_description"),
|
||||
FieldPanel("copyright_text"),
|
||||
],
|
||||
heading="Branding",
|
||||
),
|
||||
MultiFieldPanel(
|
||||
[
|
||||
FieldPanel("default_og_image"),
|
||||
FieldPanel("privacy_policy_page"),
|
||||
],
|
||||
heading="SEO & Legal",
|
||||
),
|
||||
InlinePanel("navigation_items", label="Navigation Menu Items"),
|
||||
InlinePanel("social_links", label="Social Media Links"),
|
||||
]
|
||||
|
||||
|
||||
class NavigationMenuItem(Orderable):
|
||||
settings = ParentalKey(
|
||||
SiteSettings,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="navigation_items",
|
||||
)
|
||||
link_page = models.ForeignKey(
|
||||
"wagtailcore.Page",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=SET_NULL,
|
||||
related_name="+",
|
||||
help_text="Link to an internal page. If unpublished, the link is hidden automatically.",
|
||||
)
|
||||
link_url = models.URLField(
|
||||
blank=True,
|
||||
default="",
|
||||
help_text="External URL (used only when no page is selected).",
|
||||
)
|
||||
link_title = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
default="",
|
||||
help_text="Override the display text. If blank, the page title is used.",
|
||||
)
|
||||
open_in_new_tab = models.BooleanField(default=False)
|
||||
show_in_header = models.BooleanField(default=True)
|
||||
show_in_footer = models.BooleanField(default=True)
|
||||
|
||||
panels = [
|
||||
FieldPanel("link_page"),
|
||||
FieldPanel("link_url"),
|
||||
FieldPanel("link_title"),
|
||||
FieldPanel("open_in_new_tab"),
|
||||
FieldPanel("show_in_header"),
|
||||
FieldPanel("show_in_footer"),
|
||||
]
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
if self.link_title:
|
||||
return self.link_title
|
||||
if self.link_page:
|
||||
return self.link_page.title
|
||||
return ""
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
if self.link_page:
|
||||
return self.link_page.url
|
||||
return self.link_url
|
||||
|
||||
@property
|
||||
def is_live(self):
|
||||
"""Return False if linked to an unpublished/non-live page."""
|
||||
if self.link_page_id:
|
||||
return self.link_page.live
|
||||
return bool(self.link_url)
|
||||
|
||||
class Meta(Orderable.Meta):
|
||||
pass
|
||||
|
||||
|
||||
class SocialMediaLink(Orderable):
|
||||
settings = ParentalKey(
|
||||
SiteSettings,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="social_links",
|
||||
)
|
||||
platform = models.CharField(
|
||||
max_length=30,
|
||||
choices=SOCIAL_ICON_CHOICES,
|
||||
)
|
||||
url = models.URLField()
|
||||
label = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
default="",
|
||||
help_text="Display label. If blank, the platform name is used.",
|
||||
)
|
||||
|
||||
panels = [
|
||||
FieldPanel("platform"),
|
||||
FieldPanel("url"),
|
||||
FieldPanel("label"),
|
||||
]
|
||||
|
||||
@property
|
||||
def display_label(self):
|
||||
if self.label:
|
||||
return self.label
|
||||
return dict(SOCIAL_ICON_CHOICES).get(self.platform, self.platform)
|
||||
|
||||
@property
|
||||
def icon_template(self):
|
||||
return f"components/icons/{self.platform}.html"
|
||||
|
||||
class Meta(Orderable.Meta):
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user