Add banner image support to ArticlePage and enhance embed validation
- Introduced `banner_image` field to ArticlePage model. - Updated article templates to display banner images. - Added ValidatingEmbedBlock for improved URL validation. - Refactored category block templates to use static template loading. - Enhanced header navigation to include submenu support. - Updated .gitignore to exclude media files.
This commit is contained in:
parent
3232de90d4
commit
ee6eb0db17
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
.env
|
.env
|
||||||
__pycache__
|
__pycache__
|
||||||
*.pyc
|
*.pyc
|
||||||
|
media/
|
||||||
37
innovedus_cms/home/blocks.py
Normal file
37
innovedus_cms/home/blocks.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from wagtail.embeds.blocks import EmbedBlock
|
||||||
|
from wagtail.embeds import embeds as wagtail_embeds
|
||||||
|
from wagtail import blocks
|
||||||
|
|
||||||
|
|
||||||
|
class ValidatingEmbedBlock(EmbedBlock):
|
||||||
|
"""
|
||||||
|
Embed block that validates the URL at clean-time by resolving
|
||||||
|
via Wagtail's embed system. Raises ValidationError if not embeddable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def clean(self, value):
|
||||||
|
value = super().clean(value)
|
||||||
|
if value:
|
||||||
|
try:
|
||||||
|
# Attempt to resolve and cache embed; will raise on failure
|
||||||
|
wagtail_embeds.get_embed(value)
|
||||||
|
except Exception:
|
||||||
|
raise ValidationError(
|
||||||
|
"嵌入連結無法驗證,請確認為公開且可嵌入的 URL。"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class H2HeadingBlock(blocks.CharBlock):
|
||||||
|
class Meta:
|
||||||
|
template = "home/blocks/h2_heading.html"
|
||||||
|
icon = "title"
|
||||||
|
label = "Heading"
|
||||||
|
|
||||||
|
|
||||||
|
class HorizontalRuleBlock(blocks.StaticBlock):
|
||||||
|
class Meta:
|
||||||
|
template = "home/blocks/horizontal_rule.html"
|
||||||
|
icon = "horizontalrule"
|
||||||
|
label = "Separator"
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-29 06:25
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import wagtail.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('home', '0006_articlepage_cover_image_articlepage_recommended'),
|
||||||
|
('wagtailimages', '0027_image_description'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='articlepage',
|
||||||
|
name='banner_image',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='文章頁頂部橫幅圖片', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='articlepage',
|
||||||
|
name='body',
|
||||||
|
field=wagtail.fields.StreamField([('heading', 0), ('paragraph', 1), ('image', 2), ('embed', 3), ('hr', 4), ('html', 5)], block_lookup={0: ('home.blocks.H2HeadingBlock', (), {'form_classname': 'full title'}), 1: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link', 'ol', 'ul']}), 2: ('wagtail.images.blocks.ImageChooserBlock', (), {}), 3: ('home.blocks.ValidatingEmbedBlock', (), {}), 4: ('home.blocks.HorizontalRuleBlock', (), {}), 5: ('wagtail.blocks.RawHTMLBlock', (), {'help_text': '僅限信任來源的 blockquote/iframe 原始碼'})}),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -6,19 +6,29 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
|||||||
BLOCK_SIZE = 5
|
BLOCK_SIZE = 5
|
||||||
PAGE_SIZE = 10
|
PAGE_SIZE = 10
|
||||||
|
|
||||||
|
|
||||||
class CategoryMixin:
|
class CategoryMixin:
|
||||||
def build_category_blocks(self, request=None):
|
def build_category_blocks(self, request=None):
|
||||||
blocks = []
|
blocks = []
|
||||||
subcategories = self.get_children().type(CategoryPage).live()
|
subcategories = self.get_children().type(CategoryPage).live()
|
||||||
if subcategories.exists():
|
if subcategories.exists():
|
||||||
for category in subcategories :
|
for category in subcategories:
|
||||||
blocks.append({
|
blocks.append(
|
||||||
|
{
|
||||||
"title": category.title,
|
"title": category.title,
|
||||||
"items": ArticlePage.objects.child_of(category).live().order_by("-first_published_at")[:BLOCK_SIZE],
|
"items": ArticlePage.objects.child_of(category)
|
||||||
|
.live()
|
||||||
|
.order_by("-first_published_at")[:BLOCK_SIZE],
|
||||||
"url": category.url,
|
"url": category.url,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
paginator = Paginator(ArticlePage.objects.child_of(self).live().order_by("-first_published_at"), PAGE_SIZE)
|
paginator = Paginator(
|
||||||
|
ArticlePage.objects.child_of(self)
|
||||||
|
.live()
|
||||||
|
.order_by("-first_published_at"),
|
||||||
|
PAGE_SIZE,
|
||||||
|
)
|
||||||
page_number = request.GET.get("page") if request else None
|
page_number = request.GET.get("page") if request else None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -28,23 +38,29 @@ class CategoryMixin:
|
|||||||
except EmptyPage:
|
except EmptyPage:
|
||||||
page_obj = paginator.page(paginator.num_pages)
|
page_obj = paginator.page(paginator.num_pages)
|
||||||
|
|
||||||
blocks.append({
|
blocks.append(
|
||||||
|
{
|
||||||
"title": self.title,
|
"title": self.title,
|
||||||
"items": page_obj,
|
"items": page_obj,
|
||||||
"url": self.url,
|
"url": self.url,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
return blocks
|
return blocks
|
||||||
|
|
||||||
def get_latest_articles(self, request=None):
|
def get_latest_articles(self, request=None):
|
||||||
latestPage = LatestPage.objects.first()
|
latest_page = LatestPage.objects.first()
|
||||||
if not request:
|
if not request:
|
||||||
return {
|
return {
|
||||||
"title": latestPage.title,
|
"title": latest_page.title if latest_page else "最新文章",
|
||||||
"items": ArticlePage.objects.live().order_by("-first_published_at")[:BLOCK_SIZE],
|
"items": ArticlePage.objects.live().order_by("-first_published_at")[
|
||||||
"url": latestPage.url,
|
:BLOCK_SIZE
|
||||||
|
],
|
||||||
|
"url": latest_page.url if latest_page else "#",
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
paginator = Paginator(ArticlePage.objects.live().order_by("-first_published_at"), PAGE_SIZE)
|
paginator = Paginator(
|
||||||
|
ArticlePage.objects.live().order_by("-first_published_at"), PAGE_SIZE
|
||||||
|
)
|
||||||
page_number = request.GET.get("page")
|
page_number = request.GET.get("page")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -60,12 +76,12 @@ class CategoryMixin:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def get_recommended_articles(self, request=None):
|
def get_recommended_articles(self, request=None):
|
||||||
recommendedPage = RecommendedPage.objects.first()
|
recommended_page = RecommendedPage.objects.first()
|
||||||
if not request:
|
if not request:
|
||||||
return {
|
return {
|
||||||
"title": recommendedPage.title,
|
"title": recommended_page.title if recommended_page else "推薦文章",
|
||||||
"items": ArticlePage.objects.filter(recommended=True).live()[:BLOCK_SIZE],
|
"items": ArticlePage.objects.filter(recommended=True).live()[:BLOCK_SIZE],
|
||||||
"url": recommendedPage.url,
|
"url": recommended_page.url if recommended_page else "#",
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
paginator = Paginator(ArticlePage.objects.filter(recommended=True).live(), PAGE_SIZE)
|
paginator = Paginator(ArticlePage.objects.filter(recommended=True).live(), PAGE_SIZE)
|
||||||
@ -82,54 +98,54 @@ class CategoryMixin:
|
|||||||
"items": page_obj,
|
"items": page_obj,
|
||||||
"url": self.url,
|
"url": self.url,
|
||||||
}
|
}
|
||||||
return blocks
|
|
||||||
|
|
||||||
class HomePage(Page, CategoryMixin):
|
class HomePage(Page, CategoryMixin):
|
||||||
def get_context(self, request):
|
def get_context(self, request):
|
||||||
context = super().get_context(request)
|
context = super().get_context(request)
|
||||||
|
|
||||||
category_blocks = [
|
category_blocks = [self.get_latest_articles(), self.get_recommended_articles()]
|
||||||
self.get_latest_articles(),
|
|
||||||
self.get_recommended_articles(),
|
|
||||||
]
|
|
||||||
|
|
||||||
# 找出第一層 CategoryPage(HomePage 直屬子項)
|
# 找出第一層 CategoryPage(HomePage 直屬子頁)
|
||||||
categories = CategoryPage.objects.child_of(self).live().in_menu()
|
categories = CategoryPage.objects.child_of(self).live().in_menu()
|
||||||
|
|
||||||
# 若第一層沒有分類,就嘗試抓所有 descendant CategoryPage
|
# 若第一層沒有,抓 descendant CategoryPage
|
||||||
if not categories.exists():
|
if not categories.exists():
|
||||||
categories = CategoryPage.objects.descendant_of(self).live().in_menu()
|
categories = CategoryPage.objects.descendant_of(self).live().in_menu()
|
||||||
|
|
||||||
for category in categories:
|
for category in categories:
|
||||||
subcategories = category.get_children().type(CategoryPage).live()
|
subcategories = category.get_children().type(CategoryPage).live()
|
||||||
category_blocks.append({
|
category_blocks.append(
|
||||||
|
{
|
||||||
"title": category.title,
|
"title": category.title,
|
||||||
"type": "category",
|
"type": "category",
|
||||||
"items": subcategories or ArticlePage.objects.child_of(category).live()[:BLOCK_SIZE],
|
"items": ArticlePage.objects.child_of(category).live()[:BLOCK_SIZE],
|
||||||
"url": category.url,
|
"url": category.url,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
context["category_blocks"] = category_blocks
|
context["category_blocks"] = category_blocks
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class LatestPage(Page, CategoryMixin):
|
class LatestPage(Page, CategoryMixin):
|
||||||
template = "home/category_page.html"
|
template = "home/category_page.html"
|
||||||
|
|
||||||
def get_context(self, request):
|
def get_context(self, request):
|
||||||
context = super().get_context(request)
|
context = super().get_context(request)
|
||||||
context["category_blocks"] = [
|
context["category_blocks"] = [self.get_latest_articles(request)]
|
||||||
self.get_latest_articles(request)
|
|
||||||
]
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class RecommendedPage(Page, CategoryMixin):
|
class RecommendedPage(Page, CategoryMixin):
|
||||||
template = "home/category_page.html"
|
template = "home/category_page.html"
|
||||||
|
|
||||||
def get_context(self, request):
|
def get_context(self, request):
|
||||||
context = super().get_context(request)
|
context = super().get_context(request)
|
||||||
context["category_blocks"] = [
|
context["category_blocks"] = [self.get_recommended_articles(request)]
|
||||||
self.get_recommended_articles(request)
|
|
||||||
]
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class CategoryPage(Page, CategoryMixin):
|
class CategoryPage(Page, CategoryMixin):
|
||||||
@property
|
@property
|
||||||
def has_subcategories(self):
|
def has_subcategories(self):
|
||||||
@ -140,35 +156,52 @@ class CategoryPage(Page, CategoryMixin):
|
|||||||
context["category_blocks"] = self.build_category_blocks(request)
|
context["category_blocks"] = self.build_category_blocks(request)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
# from wagtail.fields import RichTextField
|
|
||||||
from wagtail.admin.panels import FieldPanel
|
from wagtail.admin.panels import FieldPanel
|
||||||
from wagtail import blocks
|
from wagtail import blocks
|
||||||
from wagtail.embeds.blocks import EmbedBlock
|
|
||||||
from wagtail.images.blocks import ImageChooserBlock
|
from wagtail.images.blocks import ImageChooserBlock
|
||||||
from wagtail.fields import StreamField
|
from wagtail.fields import StreamField
|
||||||
|
from .blocks import ValidatingEmbedBlock, H2HeadingBlock, HorizontalRuleBlock
|
||||||
|
|
||||||
|
|
||||||
class ArticlePage(Page):
|
class ArticlePage(Page):
|
||||||
cover_image = models.ForeignKey(
|
cover_image = models.ForeignKey(
|
||||||
"wagtailimages.Image",
|
"wagtailimages.Image",
|
||||||
null=True, blank=True,
|
null=True,
|
||||||
|
blank=True,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name="+",
|
related_name="+",
|
||||||
help_text="文章列表與分享用的首圖"
|
help_text="列表封面圖",
|
||||||
|
)
|
||||||
|
banner_image = models.ForeignKey(
|
||||||
|
"wagtailimages.Image",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="+",
|
||||||
|
help_text="文章內文橫幅圖片",
|
||||||
)
|
)
|
||||||
date = models.DateField("Published date")
|
date = models.DateField("Published date")
|
||||||
intro = models.CharField(max_length=250, blank=True)
|
intro = models.CharField(max_length=250, blank=True)
|
||||||
body = StreamField([
|
body = StreamField(
|
||||||
("heading", blocks.CharBlock(form_classname="full title")),
|
[
|
||||||
|
("heading", H2HeadingBlock(form_classname="full title")),
|
||||||
("paragraph", blocks.RichTextBlock(features=["bold", "italic", "link"])),
|
("paragraph", blocks.RichTextBlock(features=["bold", "italic", "link"])),
|
||||||
("image", ImageChooserBlock()),
|
("image", ImageChooserBlock()),
|
||||||
("embed", EmbedBlock()),
|
("embed", ValidatingEmbedBlock()),
|
||||||
], use_json_field=True)
|
("hr", HorizontalRuleBlock()),
|
||||||
recommended = models.BooleanField(default=False, help_text="在推薦清單顯示")
|
("html", blocks.RawHTMLBlock(help_text="僅限信任來源的 blockquote/iframe 原始碼")),
|
||||||
|
],
|
||||||
|
use_json_field=True,
|
||||||
|
)
|
||||||
|
recommended = models.BooleanField(default=False, help_text="在推薦區塊顯示")
|
||||||
|
|
||||||
content_panels = Page.content_panels + [
|
content_panels = Page.content_panels + [
|
||||||
FieldPanel("recommended"),
|
FieldPanel("recommended"),
|
||||||
FieldPanel("cover_image"),
|
FieldPanel("cover_image"),
|
||||||
|
FieldPanel("banner_image"),
|
||||||
FieldPanel("date"),
|
FieldPanel("date"),
|
||||||
FieldPanel("intro"),
|
FieldPanel("intro"),
|
||||||
FieldPanel("body"),
|
FieldPanel("body"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,11 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<article>
|
<article>
|
||||||
{% image page.cover_image original as cover %}
|
|
||||||
<img src="{{ cover.url }}" alt="{{ page.title }}">
|
|
||||||
<h1>{{ page.title }}</h1>
|
<h1>{{ page.title }}</h1>
|
||||||
|
{% if page.banner_image %}
|
||||||
|
{% image page.banner_image original as banner %}
|
||||||
|
<img src="{{ banner.url }}" alt="{{ page.title }}">
|
||||||
|
{% endif %}
|
||||||
<p class="date">{{ page.date }}</p>
|
<p class="date">{{ page.date }}</p>
|
||||||
<div class="intro">{{ page.intro }}</div>
|
<div class="intro">{{ page.intro }}</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
|
|||||||
1
innovedus_cms/home/templates/home/blocks/h2_heading.html
Normal file
1
innovedus_cms/home/templates/home/blocks/h2_heading.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<h2 class="article-heading">{{ value }}</h2>
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<hr class="article-hr">
|
||||||
@ -1,4 +1,4 @@
|
|||||||
{% load wagtailimages_tags %}
|
{% load wagtailimages_tags static %}
|
||||||
|
|
||||||
|
|
||||||
<div class="category-block-list">
|
<div class="category-block-list">
|
||||||
@ -13,7 +13,7 @@
|
|||||||
{% image article.cover_image max-200x200 as cover %}
|
{% image article.cover_image max-200x200 as cover %}
|
||||||
<img src="{{ cover.url }}" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:contain;display:block;"/>
|
<img src="{{ cover.url }}" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:contain;display:block;"/>
|
||||||
{% else %}
|
{% else %}
|
||||||
<img src="/static/img/default_cover.jpg" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:contain;display:block;"/>
|
<img src="{% static 'img/default_cover.jpg' %}" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:contain;display:block;"/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ article.title }}
|
{{ article.title }}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
{% load wagtailimages_tags %}
|
{% load wagtailimages_tags static %}
|
||||||
|
|
||||||
<div class="category-full-list">
|
<div class="category-full-list">
|
||||||
{% with category=category_blocks.0 %}
|
{% with category=category_blocks.0 %}
|
||||||
@ -13,7 +13,7 @@
|
|||||||
{% image article.cover_image max-200x200 as cover %}
|
{% image article.cover_image max-200x200 as cover %}
|
||||||
<img src="{{ cover.url }}" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:contain;display:block;"/>
|
<img src="{{ cover.url }}" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:contain;display:block;"/>
|
||||||
{% else %}
|
{% else %}
|
||||||
<img src="/static/img/default_cover.jpg" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:contain;display:block;"/>
|
<img src="{% static 'img/default_cover.jpg' %}" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:contain;display:block;"/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ article.title }}
|
{{ article.title }}
|
||||||
</a>
|
</a>
|
||||||
@ -26,14 +26,15 @@
|
|||||||
{% if category.items.paginator.num_pages > 1 %}
|
{% if category.items.paginator.num_pages > 1 %}
|
||||||
<div class="pagination">
|
<div class="pagination">
|
||||||
{% if category.items.has_previous %}
|
{% if category.items.has_previous %}
|
||||||
<a href="?page={{ category.items.previous_page_number }}">← 上一頁</a>
|
<a href="?page={{ category.items.previous_page_number }}">上一頁</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span>第 {{ category.items.number }} / {{ category.items.paginator.num_pages }} 頁</span>
|
<span>第 {{ category.items.number }} / {{ category.items.paginator.num_pages }} 頁</span>
|
||||||
{% if category.items.has_next %}
|
{% if category.items.has_next %}
|
||||||
<a href="?page={{ category.items.next_page_number }}">下一頁 →</a>
|
<a href="?page={{ category.items.next_page_number }}">下一頁</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -124,6 +124,28 @@ USE_I18N = True
|
|||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
|
# --- Wagtail embeds (Instagram/FB via Graph oEmbed) ---
|
||||||
|
# Reads a token from env and adds an extra oEmbed finder for IG/FB.
|
||||||
|
# Keeps the default oEmbed finder first for YouTube/Vimeo/etc.
|
||||||
|
WAGTAIL_EMBED_FINDERS = [
|
||||||
|
{"class": "wagtail.embeds.finders.oembed"},
|
||||||
|
{
|
||||||
|
"class": "wagtail.embeds.finders.oembed",
|
||||||
|
"options": {
|
||||||
|
"providers": [
|
||||||
|
{"endpoint": "https://graph.facebook.com/v11.0/instagram_oembed", "urls": ["https://www.instagram.com/*"]},
|
||||||
|
{"endpoint": "https://graph.facebook.com/v11.0/oembed_post", "urls": ["https://www.facebook.com/*"]},
|
||||||
|
{"endpoint": "https://graph.facebook.com/v11.0/oembed_page", "urls": ["https://www.facebook.com/*"]},
|
||||||
|
{"endpoint": "https://graph.facebook.com/v11.0/oembed_video", "urls": ["https://www.facebook.com/*"]},
|
||||||
|
],
|
||||||
|
"params": {
|
||||||
|
"access_token": os.environ.get("IG_OEMBED_ACCESS_TOKEN", ""),
|
||||||
|
"omitscript": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
LANGUAGES = [
|
LANGUAGES = [
|
||||||
|
|||||||
@ -42,6 +42,8 @@
|
|||||||
|
|
||||||
{# Global javascript #}
|
{# Global javascript #}
|
||||||
<script type="text/javascript" src="{% static 'js/mysite.js' %}"></script>
|
<script type="text/javascript" src="{% static 'js/mysite.js' %}"></script>
|
||||||
|
{# Instagram embed script to render IG oEmbeds #}
|
||||||
|
<script async src="https://www.instagram.com/embed.js"></script>
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
{# Override this in templates to add extra javascript #}
|
{# Override this in templates to add extra javascript #}
|
||||||
|
|||||||
@ -14,11 +14,26 @@
|
|||||||
|
|
||||||
<nav class="main-nav">
|
<nav class="main-nav">
|
||||||
<ul>
|
<ul>
|
||||||
{%with page.get_site.root_page as root %}
|
{% with site_root=page.get_site.root_page %}
|
||||||
{% for page in root.get_descendants.live.in_menu %}
|
{# Top-level menu: direct children of site root #}
|
||||||
<li><a href="{{ page.url }}">{{ page.title }}</a></li>
|
{% for menu_page in site_root.get_children.live.in_menu %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ menu_page.url }}">{{ menu_page.title }}</a>
|
||||||
|
{# Second-level: direct children of each top-level page #}
|
||||||
|
{% with submenu=menu_page.get_children.live.in_menu %}
|
||||||
|
{% if submenu %}
|
||||||
|
<ul class="submenu">
|
||||||
|
{% for subpage in submenu %}
|
||||||
|
<li><a href="{{ subpage.url }}">{{ subpage.title }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
|
{# Optional extra links from settings #}
|
||||||
{% if settings.base.HeaderSettings.main_links %}
|
{% if settings.base.HeaderSettings.main_links %}
|
||||||
{% for item in settings.base.HeaderSettings.main_links %}
|
{% for item in settings.base.HeaderSettings.main_links %}
|
||||||
<li><a href="{{ item.value.url }}">{{ item.value.label }}</a></li>
|
<li><a href="{{ item.value.url }}">{{ item.value.label }}</a></li>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user