Add tagging and trending functionality to ArticlePage

- Created ArticlePageTag model for tagging articles.
- Added ClusterTaggableManager to ArticlePage for tag management.
- Renamed 'recommended' field to 'trending' in ArticlePage.
- Updated migrations to reflect changes in models.
- Implemented methods to retrieve trending articles and related articles based on tags.
- Modified templates to display tags and related articles on article pages.
- Refactored category and homepage templates to accommodate new sections and layouts.
- Removed deprecated templates and added new includes for better modularity.
- Updated header to include links for latest and trending articles.
- Added environment variable support for configurable block and page sizes.
- Updated requirements to include python-dotenv for environment variable management.
This commit is contained in:
Warren Chen 2025-11-06 16:49:31 +09:00
parent 7c9fe7f6f9
commit b04ad110a6
21 changed files with 433 additions and 159 deletions

View File

@ -0,0 +1,11 @@
from home.models import LatestPage, TrendingPage
def navigation_pages(request):
"""
Provide Latest/Trending page references for site-wide navigation.
"""
return {
"nav_latest_page": LatestPage.objects.live().first(),
"nav_trending_page": TrendingPage.objects.live().first(),
}

View File

@ -0,0 +1,59 @@
import django.db.models.deletion
import modelcluster.contrib.taggit
import modelcluster.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("home", "0008_alter_articlepage_banner_image_and_more"),
("taggit", "0004_alter_taggeditem_content_type_alter_taggeditem_tag"),
]
operations = [
migrations.CreateModel(
name="ArticlePageTag",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"content_object",
modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="tagged_items",
to="home.articlepage",
),
),
(
"tag",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="home_articlepagetag_items",
to="taggit.tag",
),
),
],
options={
"abstract": False,
},
),
migrations.AddField(
model_name="articlepage",
name="tags",
field=modelcluster.contrib.taggit.ClusterTaggableManager(
blank=True,
help_text="A comma-separated list of tags.",
through="home.ArticlePageTag",
to="taggit.Tag",
verbose_name="Tags",
),
),
]

View File

@ -0,0 +1,20 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("home", "0009_articlepagetag_articlepage_tags"),
]
operations = [
migrations.AlterField(
model_name="articlepage",
name="recommended",
field=models.BooleanField(
default=False,
help_text="在熱門區塊顯示",
verbose_name="Trending",
),
),
]

View File

@ -0,0 +1,15 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("home", "0010_alter_articlepage_recommended"),
]
operations = [
migrations.RenameModel(
old_name="RecommendedPage",
new_name="TrendingPage",
),
]

View File

@ -0,0 +1,30 @@
# Generated by Django 5.2.7 on 2025-11-06 04:04
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0011_rename_recommendedpage_trendingpage'),
('taggit', '0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx'),
]
operations = [
migrations.RenameField(
model_name='articlepage',
old_name='recommended',
new_name='trending',
),
migrations.AlterField(
model_name='articlepagetag',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='articlepagetag',
name='tag',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_items', to='taggit.tag'),
),
]

View File

@ -1,28 +1,47 @@
import os
from django.db import models from django.db import models
from wagtail.models import Page from wagtail.models import Page
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from modelcluster.contrib.taggit import ClusterTaggableManager
from modelcluster.fields import ParentalKey
from taggit.models import TaggedItemBase
BLOCK_SIZE = 5 def _get_env_int(name, default):
PAGE_SIZE = 10 value = os.environ.get(name)
if value is None:
return default
try:
return int(value)
except ValueError:
return default
BLOCK_SIZE = _get_env_int("HOMEPAGE_BLOCK_SIZE", 5) # Default to 5 articles in block layout
HORIZON_SIZE = _get_env_int("HOMEPAGE_HORIZON_SIZE", 8) # Default to 8 articles in horizon layout
PAGE_SIZE = _get_env_int("HOMEPAGE_PAGE_SIZE", 10) # Default to 10 articles per page for pagination
# Mixin for Category-related functionality
class CategoryMixin: class CategoryMixin:
# Build category blocks
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():
# If there are subcategories, create blocks for each
for category in subcategories: for category in subcategories:
blocks.append( blocks.append(
{ {
"title": category.title, "title": category.title,
"items": ArticlePage.objects.child_of(category) "items": ArticlePage.objects.child_of(category)
.live() .live()
.order_by("-first_published_at")[:BLOCK_SIZE], .order_by("-first_published_at")[:HORIZON_SIZE],
"url": category.url, "url": category.url,
"layout": "horizon",
} }
) )
else: else:
# If no subcategories, paginate articles under this category
paginator = Paginator( paginator = Paginator(
ArticlePage.objects.child_of(self) ArticlePage.objects.child_of(self)
.live() .live()
@ -47,17 +66,31 @@ class CategoryMixin:
) )
return blocks return blocks
# Build breadcrumbs
def build_breadcrumbs(self):
site = self.get_site()
site_root = site.root_page if site else None
if site_root:
ancestors = self.get_ancestors().specific().filter(depth__gt=site_root.depth)
else:
ancestors = self.get_ancestors().specific()
breadcrumbs = list(ancestors) + [self]
return breadcrumbs, site_root
# Get latest articles
def get_latest_articles(self, request=None): def get_latest_articles(self, request=None):
latest_page = LatestPage.objects.first() latest_page = LatestPage.objects.first()
if not request: if not request:
# No request means no pagination (e.g., homepage)
return { return {
"title": latest_page.title if latest_page else "最新文章", "title": latest_page.title,
"items": ArticlePage.objects.live().order_by("-first_published_at")[ "items": ArticlePage.objects.live().order_by("-first_published_at")[
:BLOCK_SIZE :BLOCK_SIZE
], ],
"url": latest_page.url if latest_page else "#", "url": latest_page.url,
} }
else: else:
# Paginated view
paginator = Paginator( paginator = Paginator(
ArticlePage.objects.live().order_by("-first_published_at"), PAGE_SIZE ArticlePage.objects.live().order_by("-first_published_at"), PAGE_SIZE
) )
@ -75,16 +108,26 @@ class CategoryMixin:
"url": self.url, "url": self.url,
} }
def get_recommended_articles(self, request=None): def get_trending_articles(self, request=None, exclude_ids=None):
recommended_page = RecommendedPage.objects.first() trending_page = TrendingPage.objects.first()
articles_qs = ArticlePage.objects.filter(trending=True).live().order_by(
"-first_published_at"
)
# Exclude specified article IDs
if exclude_ids:
articles_qs = articles_qs.exclude(id__in=exclude_ids)
if not request: if not request:
# No request means no pagination (e.g., homepage)
return { return {
"title": recommended_page.title if recommended_page else "推薦文章", "title": trending_page.title,
"items": ArticlePage.objects.filter(recommended=True).live()[:BLOCK_SIZE], "items": articles_qs[:HORIZON_SIZE],
"url": recommended_page.url if recommended_page else "#", "url": trending_page.url,
} }
else: else:
paginator = Paginator(ArticlePage.objects.filter(recommended=True).live(), PAGE_SIZE) # Paginated view
paginator = Paginator(articles_qs, PAGE_SIZE)
page_number = request.GET.get("page") page_number = request.GET.get("page")
try: try:
@ -104,27 +147,43 @@ 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 = [self.get_latest_articles(), self.get_recommended_articles()] sections = {
"top_section": [],
"category_sections": [],
}
# 找出第一層 CategoryPageHomePage 直屬子頁) latest_section = self.get_latest_articles().copy()
latest_section["layout"] = "block"
sections["top_section"].append(latest_section)
# Exclude latest articles from trending section
latest_items = latest_section.get("items", [])
if hasattr(latest_items, "values_list"):
latest_ids = list(latest_items.values_list("id", flat=True))
else:
latest_ids = [item.id for item in latest_items]
trending_section = self.get_trending_articles(
exclude_ids=latest_ids
).copy()
trending_section["layout"] = "horizon"
sections["top_section"].append(trending_section)
# Build category sections
categories = CategoryPage.objects.child_of(self).live().in_menu() categories = CategoryPage.objects.child_of(self).live().in_menu()
# 若第一層沒有,抓 descendant CategoryPage
if not categories.exists():
categories = CategoryPage.objects.descendant_of(self).live().in_menu()
for category in categories: for category in categories:
subcategories = category.get_children().type(CategoryPage).live() sections["category_sections"].append(
category_blocks.append(
{ {
"title": category.title, "title": category.title,
"type": "category",
"items": ArticlePage.objects.child_of(category).live()[:BLOCK_SIZE],
"url": category.url, "url": category.url,
"items": ArticlePage.objects.descendant_of(category)
.live()
.order_by("-first_published_at")[:HORIZON_SIZE],
"layout": "horizon",
} }
) )
context["category_blocks"] = category_blocks context["sections"] = sections
return context return context
@ -133,16 +192,22 @@ class LatestPage(Page, CategoryMixin):
def get_context(self, request): def get_context(self, request):
context = super().get_context(request) context = super().get_context(request)
context["category_blocks"] = [self.get_latest_articles(request)] context["category_sections"] = [self.get_latest_articles(request)]
breadcrumbs, site_root = self.build_breadcrumbs()
context["breadcrumbs"] = breadcrumbs
context["breadcrumb_root"] = site_root
return context return context
class RecommendedPage(Page, CategoryMixin): class TrendingPage(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"] = [self.get_recommended_articles(request)] context["category_sections"] = [self.get_trending_articles(request)]
breadcrumbs, site_root = self.build_breadcrumbs()
context["breadcrumbs"] = breadcrumbs
context["breadcrumb_root"] = site_root
return context return context
@ -153,7 +218,10 @@ class CategoryPage(Page, CategoryMixin):
def get_context(self, request): def get_context(self, request):
context = super().get_context(request) context = super().get_context(request)
context["category_blocks"] = self.build_category_blocks(request) context["category_sections"] = self.build_category_blocks(request)
breadcrumbs, site_root = self.build_breadcrumbs()
context["breadcrumbs"] = breadcrumbs
context["breadcrumb_root"] = site_root
return context return context
@ -163,6 +231,13 @@ from wagtail.images.blocks import ImageChooserBlock
from wagtail.fields import StreamField from wagtail.fields import StreamField
from .blocks import ValidatingEmbedBlock, H2HeadingBlock, HorizontalRuleBlock from .blocks import ValidatingEmbedBlock, H2HeadingBlock, HorizontalRuleBlock
# HashTag for Article
class ArticlePageTag(TaggedItemBase):
content_object = ParentalKey(
"home.ArticlePage",
related_name="tagged_items",
on_delete=models.CASCADE,
)
class ArticlePage(Page): class ArticlePage(Page):
cover_image = models.ForeignKey( cover_image = models.ForeignKey(
@ -194,14 +269,33 @@ class ArticlePage(Page):
], ],
use_json_field=True, use_json_field=True,
) )
recommended = models.BooleanField(default=False, help_text="在推薦區塊顯示") trending = models.BooleanField("Trending", default=False, help_text="在熱門區塊顯示")
tags = ClusterTaggableManager(through="home.ArticlePageTag", blank=True)
content_panels = Page.content_panels + [ content_panels = Page.content_panels + [
FieldPanel("recommended"), FieldPanel("trending"),
FieldPanel("cover_image"), FieldPanel("cover_image"),
FieldPanel("banner_image"), FieldPanel("banner_image"),
FieldPanel("date"), FieldPanel("date"),
FieldPanel("intro"), FieldPanel("intro"),
FieldPanel("body"), FieldPanel("body"),
FieldPanel("tags"),
] ]
def get_context(self, request):
context = super().get_context(request)
tag_ids = list(self.tags.values_list("id", flat=True))
if tag_ids:
related_articles = (
ArticlePage.objects.live()
.exclude(id=self.id)
.filter(tags__id__in=tag_ids)
.distinct()
.order_by("-first_published_at")[:4]
)
else:
related_articles = ArticlePage.objects.none()
context["related_articles"] = related_articles
return context

View File

@ -13,5 +13,23 @@
<div class="body"> <div class="body">
{{ page.body }} {{ page.body }}
</div> </div>
{% with tags=page.tags.all %}
{% if tags %}
<div class="tags">
<span>Hashtags:</span>
<ul>
{% for tag in tags %}
<li>#{{ tag }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endwith %}
{% if related_articles %}
<section class="related-articles">
<h2>相關文章</h2>
{% include "home/includes/article_list.html" with items=related_articles %}
</section>
{% endif %}
</article> </article>
{% endblock %} {% endblock %}

View File

@ -1,9 +1,31 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load wagtailcore_tags %} {% load wagtailcore_tags %}
{% block content %} {% block content %}
{% if breadcrumbs %}
<nav class="breadcrumbs" aria-label="breadcrumb">
<ol>
{% if breadcrumb_root %}
<li><a href="{{ breadcrumb_root.url }}">首頁</a></li>
{% endif %}
{% for crumb in breadcrumbs %}
{% if not breadcrumb_root or crumb.id != breadcrumb_root.id %}
<li>
{% if crumb.id == page.id %}
<span>{{ crumb.title }}</span>
{% else %}
<a href="{{ crumb.url }}">{{ crumb.title }}</a>
{% endif %}
</li>
{% endif %}
{% endfor %}
</ol>
</nav>
{% endif %}
{% if page.has_subcategories %} {% if page.has_subcategories %}
{% include "home/includes/category_block_list.html" %} {% for section in category_sections %}
{% include "home/includes/category_session.html" with section=section %}
{% endfor %}
{% else %} {% else %}
{% include "home/includes/category_full_list.html" %} {% include "home/includes/page-article-list.html" with category=category_sections.0 %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -1,14 +1,20 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %}
{% block body_class %}template-homepage{% endblock %} {% block body_class %}template-homepage{% endblock %}
{% block extra_css %}
{% endblock extra_css %}
{% block content %} {% block content %}
{% with top_section=sections.top_section %}
<h2>
<a href="{{ top_section.0.url }}">最新文章</a>
</h2>
{% for section in top_section %}
{% if section.layout == "block" %}
{% include "home/includes/block_list.html" with items=section.items %}
{% elif section.layout == "horizon" %}
{% include "home/includes/horizontal_list.html" with items=section.items %}
{% endif %}
{% endfor %}
{% endwith %}
{% include "home/includes/category_block_list.html" with category_blocks=category_blocks %} {% for section in sections.category_sections %}
{% include "home/includes/category_session.html" with section=section %}
{% endblock content %} {% endfor %}
{% endblock content %}

View File

@ -0,0 +1,21 @@
{% load wagtailimages_tags static %}
<ul class="article-list">
{% for article in items %}
<li>
<article>
<a href="{{ article.url }}">
{% if article.cover_image %}
{% 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;"/>
{% 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;"/>
{% endif %}
{{ article.title }}
</a>
</article>
</li>
{% empty %}
<li class="empty">目前沒有文章</li>
{% endfor %}
</ul>

View File

@ -0,0 +1,19 @@
{% load wagtailimages_tags static %}
<ul class="block-list">
{% for article in items %}
<li>
<a href="{{ article.url }}">
{% if article.cover_image %}
{% 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;"/>
{% 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;"/>
{% endif %}
{{ article.title }}
</a>
</li>
{% empty %}
<li class="empty">目前沒有文章</li>
{% endfor %}
</ul>

View File

@ -1,25 +0,0 @@
{% load wagtailimages_tags static %}
<div class="category-block-list">
{% for category in category_blocks %}
<section class="category-section">
<h2><a href="{{ category.url }}">{{ category.title }}</a></h2>
<ul>
{% for article in category.items %}
<li>
<a href="{{ article.url }}">
{% if article.cover_image %}
{% 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;"/>
{% 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;"/>
{% endif %}
{{ article.title }}
</a>
</li>
{% endfor %}
</ul>
</section>
{% endfor %}
</div>

View File

@ -1,40 +0,0 @@
{% load wagtailimages_tags static %}
<div class="category-full-list">
{% with category=category_blocks.0 %}
<h2><a href="{{ category.url }}">{{ category.title }}</a></h2>
<ul>
{% for article in category.items %}
<article>
<li>
<a href="{{ article.url }}">
{% if article.cover_image %}
{% 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;"/>
{% 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;"/>
{% endif %}
{{ article.title }}
</a>
</li>
<!-- <p>{{ article.search_description }}</p> -->
</article>
{% endfor %}
</ul>
{% if category.items.paginator.num_pages > 1 %}
<div class="pagination">
{% if category.items.has_previous %}
<a href="?page={{ category.items.previous_page_number }}">上一頁</a>
{% endif %}
<span>第 {{ category.items.number }} / {{ category.items.paginator.num_pages }} 頁</span>
{% if category.items.has_next %}
<a href="?page={{ category.items.next_page_number }}">下一頁</a>
{% endif %}
</div>
{% endif %}
{% endwith %}
</div>

View File

@ -0,0 +1,17 @@
{% load wagtailimages_tags static %}
<section class="article-section article-section--{{ section.layout }}">
<h2>
{% if section.url %}
<a href="{{ section.url }}">{{ section.title }}</a>
{% else %}
{{ section.title }}
{% endif %}
</h2>
{% if section.layout == "block" %}
{% include "home/includes/block_list.html" with items=section.items %}
{% elif section.layout == "horizon" %}
{% include "home/includes/horizontal_list.html" with items=section.items %}
{% endif %}
</section>

View File

@ -0,0 +1,19 @@
{% load wagtailimages_tags static %}
<ul class="horizontal-list">
{% for article in items %}
<li>
<a href="{{ article.url }}">
{% if article.cover_image %}
{% 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:cover;display:block;"/>
{% else %}
<img src="{% static 'img/default_cover.jpg' %}" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:cover;display:block;"/>
{% endif %}
<span>{{ article.title }}</span>
</a>
</li>
{% empty %}
<li class="empty">目前沒有文章</li>
{% endfor %}
</ul>

View File

@ -0,0 +1,22 @@
{% load wagtailimages_tags static %}
<div class="page-article-list">
{% with category=category_sections.0 %}
<h2><a href="{{ category.url }}">{{ category.title }}</a></h2>
{% include "home/includes/article_list.html" with items=category.items %}
{% if category.items.paginator.num_pages > 1 %}
<div class="pagination">
{% if category.items.has_previous %}
<a href="?page={{ category.items.previous_page_number }}">上一頁</a>
{% endif %}
<span>第 {{ category.items.number }} / {{ category.items.paginator.num_pages }} 頁</span>
{% if category.items.has_next %}
<a href="?page={{ category.items.next_page_number }}">下一頁</a>
{% endif %}
</div>
{% endif %}
{% endwith %}
</div>

View File

@ -1,52 +0,0 @@
{% load i18n wagtailcore_tags %}
<header class="header">
<div class="logo">
<a href="https://wagtail.org/">
<svg class="figure-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 342.5 126.2"><title>{% trans "Visit the Wagtail website" %}</title><path fill="#FFF" d="M84 1.9v5.7s-10.2-3.8-16.8 3.1c-4.8 5-5.2 10.6-3 18.1 21.6 0 25 12.1 25 12.1L87 27l6.8-8.3c0-9.8-8.1-16.3-9.8-16.8z"/><circle cx="85.9" cy="15.9" r="2.6"/><path d="M89.2 40.9s-3.3-16.6-24.9-12.1c-2.2-7.5-1.8-13 3-18.1C73.8 3.8 84 7.6 84 7.6V1.9C80.4.3 77 0 73.2 0 59.3 0 51.6 10.4 48.3 17.4L9.2 89.3l11-2.1-20.2 39 14.1-2.5L24.9 93c30.6 0 69.8-11 64.3-52.1z"/><path d="M102.4 27l-8.6-8.3L87 27z"/><path fill="#FFF" d="M30 84.1s1-.2 2.8-.6c1.8-.4 4.3-1 7.3-1.8 1.5-.4 3.1-.9 4.8-1.5 1.7-.6 3.5-1.2 5.2-2 1.8-.7 3.6-1.6 5.4-2.6 1.8-1 3.5-2.1 5.1-3.4.4-.3.8-.6 1.2-1l1.2-1c.7-.7 1.5-1.4 2.2-2.2.7-.7 1.3-1.5 1.9-2.3l.9-1.2.4-.6.4-.6c.2-.4.5-.8.7-1.2.2-.4.4-.8.7-1.2l.3-.6.3-.6c.2-.4.4-.8.5-1.2l.9-2.4c.2-.8.5-1.6.7-2.3.2-.7.3-1.5.5-2.1.1-.7.2-1.3.3-2 .1-.6.2-1.2.2-1.7.1-.5.1-1 .2-1.5.1-1.8.1-2.8.1-2.8l1.6.1s-.1 1.1-.2 2.9c-.1.5-.1 1-.2 1.5-.1.6-.1 1.2-.3 1.8-.1.6-.3 1.3-.4 2-.2.7-.4 1.4-.6 2.2-.2.8-.5 1.5-.8 2.4-.3.8-.6 1.6-1 2.5l-.6 1.2-.3.6-.3.6c-.2.4-.5.8-.7 1.3-.3.4-.5.8-.8 1.2-.1.2-.3.4-.4.6l-.4.6-.9 1.2c-.7.8-1.3 1.6-2.1 2.3-.7.8-1.5 1.4-2.3 2.2l-1.2 1c-.4.3-.8.6-1.3.9-1.7 1.2-3.5 2.3-5.3 3.3-1.8.9-3.7 1.8-5.5 2.5-1.8.7-3.6 1.3-5.3 1.8-1.7.5-3.3 1-4.9 1.3-3 .7-5.6 1.3-7.4 1.6-1.6.6-2.6.8-2.6.8z"/><g fill="#231F20"><path d="M127 83.9h-8.8l-12.6-36.4h7.9l9 27.5 9-27.5h7.9l9 27.5 9-27.5h7.9L153 83.9h-8.8L135.6 59 127 83.9zM200.1 83.9h-7V79c-3 3.6-7 5.4-12.1 5.4-3.8 0-6.9-1.1-9.4-3.2s-3.7-5-3.7-8.6c0-3.6 1.3-6.3 4-8 2.6-1.8 6.2-2.7 10.7-2.7h9.9v-1.4c0-4.8-2.7-7.3-8.1-7.3-3.4 0-6.9 1.2-10.5 3.7l-3.4-4.8c4.4-3.5 9.4-5.3 15.1-5.3 4.3 0 7.8 1.1 10.5 3.2 2.7 2.2 4.1 5.6 4.1 10.2v23.7zm-7.7-13.6v-3.1h-8.6c-5.5 0-8.3 1.7-8.3 5.2 0 1.8.7 3.1 2.1 4.1 1.4.9 3.3 1.4 5.7 1.4 2.4 0 4.6-.7 6.4-2.1 1.8-1.3 2.7-3.1 2.7-5.5zM241.7 47.5v31.7c0 6.4-1.7 11.3-5.2 14.5-3.5 3.2-8 4.8-13.4 4.8-5.5 0-10.4-1.7-14.8-5.1l3.6-5.8c3.6 2.7 7.1 4 10.8 4 3.6 0 6.5-.9 8.6-2.8 2.1-1.9 3.2-4.9 3.2-9v-4.7c-1.1 2.1-2.8 3.9-4.9 5.1-2.1 1.3-4.5 1.9-7.1 1.9-4.8 0-8.8-1.7-11.9-5.1-3.1-3.4-4.7-7.6-4.7-12.6s1.6-9.2 4.7-12.6c3.1-3.4 7.1-5.1 11.9-5.1 4.8 0 8.7 2 11.7 6v-5.4h7.5zm-28.4 16.8c0 3 .9 5.6 2.8 7.7 1.8 2.2 4.3 3.2 7.5 3.2 3.1 0 5.7-1 7.6-3.1 1.9-2.1 2.9-4.7 2.9-7.8 0-3.1-1-5.8-2.9-7.9-2-2.2-4.5-3.2-7.6-3.2-3.1 0-5.6 1.1-7.4 3.4-2 2.1-2.9 4.7-2.9 7.7zM260.9 53.6v18.5c0 1.7.5 3.1 1.4 4.1.9 1 2.2 1.5 3.8 1.5 1.6 0 3.2-.8 4.7-2.4l3.1 5.4c-2.7 2.4-5.7 3.6-8.9 3.6-3.3 0-6-1.1-8.3-3.4-2.3-2.3-3.5-5.3-3.5-9.1V53.6h-4.6v-6.2h4.6V36.1h7.7v11.4h9.6v6.2h-9.6zM309.5 83.9h-7V79c-3 3.6-7 5.4-12.1 5.4-3.8 0-6.9-1.1-9.4-3.2s-3.7-5-3.7-8.6c0-3.6 1.3-6.3 4-8 2.6-1.8 6.2-2.7 10.7-2.7h9.9v-1.4c0-4.8-2.7-7.3-8.1-7.3-3.4 0-6.9 1.2-10.5 3.7l-3.4-4.8c4.4-3.5 9.4-5.3 15.1-5.3 4.3 0 7.8 1.1 10.5 3.2 2.7 2.2 4.1 5.6 4.1 10.2v23.7zm-7.7-13.6v-3.1h-8.6c-5.5 0-8.3 1.7-8.3 5.2 0 1.8.7 3.1 2.1 4.1 1.4.9 3.3 1.4 5.7 1.4 2.4 0 4.6-.7 6.4-2.1 1.8-1.3 2.7-3.1 2.7-5.5zM319.3 40.2c-1-1-1.4-2.1-1.4-3.4 0-1.3.5-2.5 1.4-3.4 1-1 2.1-1.4 3.4-1.4 1.3 0 2.5.5 3.4 1.4 1 1 1.4 2.1 1.4 3.4 0 1.3-.5 2.5-1.4 3.4s-2.1 1.4-3.4 1.4c-1.3.1-2.4-.4-3.4-1.4zm7.2 43.7h-7.7V47.5h7.7v36.4zM342.5 83.9h-7.7V33.1h7.7v50.8z"/></g></svg>
</a>
</div>
<div class="header-link">
{% comment %}
This works for all cases but prerelease versions:
{% endcomment %}
<a href="{% wagtail_documentation_path %}/releases/{% wagtail_release_notes_path %}">
{% trans "View the release notes" %}
</a>
</div>
</header>
<main class="main">
<div class="figure">
<svg class="figure-space" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300" aria-hidden="true">
<path class="egg" fill="currentColor" d="M150 250c-42.741 0-75-32.693-75-90s42.913-110 75-110c32.088 0 75 52.693 75 110s-32.258 90-75 90z"/>
<ellipse fill="#ddd" cx="150" cy="270" rx="40" ry="7"/>
</svg>
</div>
<div class="main-text">
<h1>{% trans "Welcome to your new Wagtail site!" %}</h1>
<p>{% trans 'Please feel free to <a href="https://github.com/wagtail/wagtail/wiki/Slack">join our community on Slack</a>, or get started with one of the links below.' %}</p>
</div>
</main>
<footer class="footer" role="contentinfo">
<a class="option option-one" href="{% wagtail_documentation_path %}/">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 21c0 .5.4 1 1 1h4c.6 0 1-.5 1-1v-1H9v1zm3-19C8.1 2 5 5.1 5 9c0 2.4 1.2 4.5 3 5.7V17c0 .5.4 1 1 1h6c.6 0 1-.5 1-1v-2.3c1.8-1.3 3-3.4 3-5.7 0-3.9-3.1-7-7-7zm2.9 11.1l-.9.6V16h-4v-2.3l-.9-.6C7.8 12.2 7 10.6 7 9c0-2.8 2.2-5 5-5s5 2.2 5 5c0 1.6-.8 3.2-2.1 4.1z"/></svg>
<div>
<h2>{% trans "Wagtail Documentation" %}</h2>
<p>{% trans "Topics, references, & how-tos" %}</p>
</div>
</a>
<a class="option option-two" href="{% wagtail_documentation_path %}/getting_started/tutorial.html">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>
<div>
<h2>{% trans "Tutorial" %}</h2>
<p>{% trans "Build your first Wagtail site" %}</p>
</div>
</a>
<a class="option option-three" href="{% url 'wagtailadmin_home' %}">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M0 0h24v24H0z" fill="none"/><path d="M16.5 13c-1.2 0-3.07.34-4.5 1-1.43-.67-3.3-1-4.5-1C5.33 13 1 14.08 1 16.25V19h22v-2.75c0-2.17-4.33-3.25-6.5-3.25zm-4 4.5h-10v-1.25c0-.54 2.56-1.75 5-1.75s5 1.21 5 1.75v1.25zm9 0H14v-1.25c0-.46-.2-.86-.52-1.22.88-.3 1.96-.53 3.02-.53 2.44 0 5 1.21 5 1.75v1.25zM7.5 12c1.93 0 3.5-1.57 3.5-3.5S9.43 5 7.5 5 4 6.57 4 8.5 5.57 12 7.5 12zm0-5.5c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2zm9 5.5c1.93 0 3.5-1.57 3.5-3.5S18.43 5 16.5 5 13 6.57 13 8.5s1.57 3.5 3.5 3.5zm0-5.5c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2z"/></svg>
<div>
<h2>{% trans "Admin Interface" %}</h2>
<p>{% trans "Create your superuser first!" %}</p>
</div>
</a>
</footer>

View File

@ -0,0 +1 @@

View File

@ -77,6 +77,7 @@ TEMPLATES = [
"django.contrib.auth.context_processors.auth", "django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages", "django.contrib.messages.context_processors.messages",
"wagtail.contrib.settings.context_processors.settings", "wagtail.contrib.settings.context_processors.settings",
"home.context_processors.navigation_pages",
], ],
}, },
}, },

View File

@ -16,6 +16,21 @@
<ul> <ul>
{% with site_root=page.get_site.root_page %} {% with site_root=page.get_site.root_page %}
{# Top-level menu: direct children of site root #} {# Top-level menu: direct children of site root #}
<li>
<a href="#">
最新文章
</a>
{% if nav_latest_page or nav_trending_page %}
<ul class="submenu">
{% if nav_latest_page %}
<li><a href="{{ nav_latest_page.url }}">{{ nav_latest_page.title }}</a></li>
{% endif %}
{% if nav_trending_page %}
<li><a href="{{ nav_trending_page.url }}">{{ nav_trending_page.title }}</a></li>
{% endif %}
</ul>
{% endif %}
</li>
{% for menu_page in site_root.get_children.live.in_menu %} {% for menu_page in site_root.get_children.live.in_menu %}
<li> <li>
<a href="{{ menu_page.url }}">{{ menu_page.title }}</a> <a href="{{ menu_page.url }}">{{ menu_page.title }}</a>

View File

@ -2,4 +2,5 @@ Django>=5.2,<5.3
wagtail>=7.1,<7.2 wagtail>=7.1,<7.2
gunicorn gunicorn
dj-database-url dj-database-url
psycopg[binary] psycopg[binary]
python-dotenv