feat: replace hardcoded navigation with CMS-managed models
Some checks failed
CI / nightly-e2e (pull_request) Has been skipped
CI / deploy (pull_request) Has been skipped
CI / ci (pull_request) Failing after 14s
CI / pr-e2e (pull_request) Failing after 1m19s

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:
Mark
2026-03-02 18:45:03 +00:00
parent 22d596d666
commit 8138acb7f7
15 changed files with 611 additions and 29 deletions

View File

@@ -1,19 +1,20 @@
{% load core_tags %}
{% get_nav_items "footer" as footer_nav_items %}
{% get_social_links as social_links %}
<footer class="border-t border-zinc-200 dark:border-zinc-800 bg-brand-light dark:bg-brand-dark mt-12 py-12 text-center md:text-left">
<div class="max-w-7xl mx-auto px-6 grid grid-cols-1 md:grid-cols-4 gap-8">
<div class="md:col-span-2">
<a href="/" class="font-display font-bold text-2xl tracking-tight mb-4 inline-block">NO HYPE AI</a>
<a href="/" class="font-display font-bold text-2xl tracking-tight mb-4 inline-block">{{ site_settings.site_name|default:"NO HYPE AI" }}</a>
<p class="text-zinc-500 font-mono text-sm max-w-sm mx-auto md:mx-0">
In-depth reviews and benchmarks of the latest AI coding tools.<br>
Honest analysis for developers.
{{ site_settings.footer_description|default:"In-depth reviews and benchmarks of the latest AI coding tools.\nHonest analysis for developers."|linebreaksbr }}
</p>
</div>
<div>
<h4 class="font-display font-bold mb-4 uppercase text-sm tracking-widest text-zinc-400">Navigation</h4>
<ul class="space-y-2 font-mono text-sm text-zinc-500">
<li><a href="/" class="hover:text-brand-cyan transition-colors">Home</a></li>
<li><a href="/articles/" class="hover:text-brand-cyan transition-colors">Articles</a></li>
<li><a href="/about/" class="hover:text-brand-pink transition-colors">About</a></li>
{% for item in footer_nav_items %}
<li><a href="{{ item.url }}" class="hover:text-brand-cyan transition-colors"{% if item.open_in_new_tab %} target="_blank" rel="noopener noreferrer"{% endif %}>{{ item.title }}</a></li>
{% endfor %}
{% get_legal_pages as legal_pages %}
{% for page in legal_pages %}
<li><a href="{{ page.url }}" class="hover:text-brand-pink transition-colors">{{ page.title }}</a></li>
@@ -23,23 +24,19 @@
<div>
<h4 class="font-display font-bold mb-4 uppercase text-sm tracking-widest text-zinc-400">Connect</h4>
<ul class="space-y-2 font-mono text-sm text-zinc-500">
<li>
<a href="https://twitter.com/nohypeai" class="hover:text-brand-cyan transition-colors flex items-center justify-center md:justify-start gap-2">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6.633 10.25c.806 0 1.533-.446 2.031-1.08a9.041 9.041 0 0 1 2.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 0 0 .322-1.672V2.75a.75.75 0 0 1 .75-.75 2.25 2.25 0 0 1 2.25 2.25c0 1.152-.26 2.243-.723 3.218-.266.558.107 1.282.725 1.282m0 0h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 0 1-2.649 7.521c-.388.482-.987.729-1.605.729H13.48c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 0 0-1.423-.23H5.904m10.598-9.75H14.25M5.904 18.5c.083.205.173.405.27.602.197.4-.078.898-.523.898h-.908c-.889 0-1.713-.518-1.972-1.368a12 12 0 0 1-.521-3.507c0-1.553.295-3.036.831-4.398C3.387 9.953 4.167 9.5 5 9.5h1.053c.472 0 .745.556.5.96a8.958 8.958 0 0 0-1.302 4.665c0 1.194.232 2.333.654 3.375Z" /></svg>
Twitter (X)
</a>
</li>
<li>
<a href="/feed/" class="hover:text-brand-cyan transition-colors flex items-center justify-center md:justify-start gap-2">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12.75 19.5v-.75a7.5 7.5 0 0 0-7.5-7.5H4.5m0-6.75h.75c7.87 0 14.25 6.38 14.25 14.25v.75M6 18.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" /></svg>
RSS Feed
</a>
</li>
{% for link in social_links %}
<li>
<a href="{{ link.url }}" class="hover:text-brand-cyan transition-colors flex items-center justify-center md:justify-start gap-2"{% if link.url != "/feed/" %} target="_blank" rel="noopener noreferrer"{% endif %}>
{% include link.icon_template %}
{{ link.display_label }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
<div class="max-w-7xl mx-auto px-6 mt-12 pt-8 border-t border-zinc-200 dark:border-zinc-800 text-center font-mono text-xs text-zinc-500 flex flex-col md:flex-row justify-between items-center gap-4">
<p>&copy; {% now "Y" %} No Hype AI. All rights reserved.</p>
<p>Honest AI tool reviews for developers.</p>
<p>&copy; {% now "Y" %} {{ site_settings.copyright_text|default:"No Hype AI. All rights reserved." }}</p>
<p>{{ site_settings.tagline|default:"Honest AI tool reviews for developers." }}</p>
</div>
</footer>

View File

@@ -0,0 +1 @@
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15a4.5 4.5 0 0 0 4.5 4.5H18a3.75 3.75 0 0 0 1.332-7.257 3 3 0 0 0-3.758-3.848 5.25 5.25 0 0 0-10.233 2.33A4.502 4.502 0 0 0 2.25 15Z" /></svg>

After

Width:  |  Height:  |  Size: 334 B

View File

@@ -0,0 +1 @@
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5" /></svg>

After

Width:  |  Height:  |  Size: 266 B

View File

@@ -0,0 +1 @@
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 0 0 .75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 0 0-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0 1 12 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 0 1-.673-.38m0 0A2.18 2.18 0 0 1 3 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 0 1 3.413-.387m7.5 0V5.25A2.25 2.25 0 0 0 13.5 3h-3a2.25 2.25 0 0 0-2.25 2.25v.894m7.5 0a48.667 48.667 0 0 0-7.5 0M12 12.75h.008v.008H12v-.008Z" /></svg>

After

Width:  |  Height:  |  Size: 783 B

View File

@@ -0,0 +1 @@
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418" /></svg>

After

Width:  |  Height:  |  Size: 671 B

View File

@@ -0,0 +1 @@
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12.75 19.5v-.75a7.5 7.5 0 0 0-7.5-7.5H4.5m0-6.75h.75c7.87 0 14.25 6.38 14.25 14.25v.75M6 18.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" /></svg>

After

Width:  |  Height:  |  Size: 330 B

View File

@@ -0,0 +1 @@
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6.633 10.25c.806 0 1.533-.446 2.031-1.08a9.041 9.041 0 0 1 2.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 0 0 .322-1.672V2.75a.75.75 0 0 1 .75-.75 2.25 2.25 0 0 1 2.25 2.25c0 1.152-.26 2.243-.723 3.218-.266.558.107 1.282.725 1.282m0 0h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 0 1-2.649 7.521c-.388.482-.987.729-1.605.729H13.48c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 0 0-1.423-.23H5.904m10.598-9.75H14.25M5.904 18.5c.083.205.173.405.27.602.197.4-.078.898-.523.898h-.908c-.889 0-1.713-.518-1.972-1.368a12 12 0 0 1-.521-3.507c0-1.553.295-3.036.831-4.398C3.387 9.953 4.167 9.5 5 9.5h1.053c.472 0 .745.556.5.96a8.958 8.958 0 0 0-1.302 4.665c0 1.194.232 2.333.654 3.375Z" /></svg>

After

Width:  |  Height:  |  Size: 919 B

View File

@@ -0,0 +1 @@
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m15.75 10.5 4.72-8.9a.75.75 0 0 0-.53-1.1H13.5l-1.5 3-1.5-3H4.06a.75.75 0 0 0-.53 1.1l4.72 8.9-4.72 8.9a.75.75 0 0 0 .53 1.1H10.5l1.5-3 1.5 3h6.44a.75.75 0 0 0 .53-1.1l-4.72-8.9Z" /></svg>

After

Width:  |  Height:  |  Size: 374 B

View File

@@ -1,4 +1,5 @@
{% load static %}
{% load static core_tags %}
{% get_nav_items "header" as header_items %}
<nav class="sticky top-0 z-50 backdrop-blur-md bg-brand-light/80 dark:bg-brand-dark/80 border-b border-zinc-200 dark:border-zinc-800 transition-colors">
<div class="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
<!-- Logo -->
@@ -6,14 +7,14 @@
<div class="w-8 h-8 bg-brand-dark dark:bg-brand-light text-brand-light dark:text-brand-dark flex items-center justify-center font-display font-bold text-xl group-hover:rotate-12 transition-transform">
/
</div>
<span class="font-display font-bold text-2xl tracking-tight">NO HYPE AI</span>
<span class="font-display font-bold text-2xl tracking-tight">{{ site_settings.site_name|default:"NO HYPE AI" }}</span>
</a>
<!-- Desktop Links -->
<div class="hidden md:flex items-center gap-8 font-medium">
<a href="/" class="hover:text-brand-cyan transition-colors">Home</a>
<a href="/articles/" class="hover:text-brand-cyan transition-colors">Articles</a>
<a href="/about/" class="hover:text-brand-pink transition-colors">About</a>
{% for item in header_items %}
<a href="{{ item.url }}" class="hover:text-brand-cyan transition-colors"{% if item.open_in_new_tab %} target="_blank" rel="noopener noreferrer"{% endif %}>{{ item.title }}</a>
{% endfor %}
<a href="#newsletter" class="px-5 py-2.5 bg-brand-dark dark:bg-brand-light text-brand-light dark:text-brand-dark font-display font-bold hover:-translate-y-1 hover:shadow-solid-dark dark:hover:shadow-solid-light transition-all border border-transparent dark:border-zinc-700">Subscribe</a>
</div>
@@ -33,9 +34,9 @@
<!-- Mobile Menu (outside <nav> to avoid duplicate form[data-newsletter-form] in nav scope) -->
<div id="mobile-menu" class="md:hidden hidden sticky top-20 z-40 border-b border-zinc-200 dark:border-zinc-800 bg-brand-light/95 dark:bg-brand-dark/95 backdrop-blur-md">
<div class="max-w-7xl mx-auto px-6 py-4 flex flex-col gap-4">
<a href="/" class="font-medium py-2 hover:text-brand-cyan transition-colors">Home</a>
<a href="/articles/" class="font-medium py-2 hover:text-brand-cyan transition-colors">Articles</a>
<a href="/about/" class="font-medium py-2 hover:text-brand-pink transition-colors">About</a>
{% for item in header_items %}
<a href="{{ item.url }}" class="font-medium py-2 hover:text-brand-cyan transition-colors"{% if item.open_in_new_tab %} target="_blank" rel="noopener noreferrer"{% endif %}>{{ item.title }}</a>
{% endfor %}
<form method="post" action="/newsletter/subscribe/" data-newsletter-form class="space-y-2 pt-2 border-t border-zinc-200 dark:border-zinc-800" id="mobile-newsletter">
{% csrf_token %}
<input type="hidden" name="source" value="nav-mobile" />