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 wagtail.models import Page
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
PAGE_SIZE = 10
def _get_env_int(name, default):
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:
# Build category blocks
def build_category_blocks(self, request=None):
blocks = []
subcategories = self.get_children().type(CategoryPage).live()
if subcategories.exists():
# If there are subcategories, create blocks for each
for category in subcategories:
blocks.append(
{
"title": category.title,
"items": ArticlePage.objects.child_of(category)
.live()
.order_by("-first_published_at")[:BLOCK_SIZE],
.order_by("-first_published_at")[:HORIZON_SIZE],
"url": category.url,
"layout": "horizon",
}
)
else:
# If no subcategories, paginate articles under this category
paginator = Paginator(
ArticlePage.objects.child_of(self)
.live()
@ -47,17 +66,31 @@ class CategoryMixin:
)
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):
latest_page = LatestPage.objects.first()
if not request:
# No request means no pagination (e.g., homepage)
return {
"title": latest_page.title if latest_page else "最新文章",
"title": latest_page.title,
"items": ArticlePage.objects.live().order_by("-first_published_at")[
:BLOCK_SIZE
],
"url": latest_page.url if latest_page else "#",
"url": latest_page.url,
}
else:
# Paginated view
paginator = Paginator(
ArticlePage.objects.live().order_by("-first_published_at"), PAGE_SIZE
)
@ -75,16 +108,26 @@ class CategoryMixin:
"url": self.url,
}
def get_recommended_articles(self, request=None):
recommended_page = RecommendedPage.objects.first()
def get_trending_articles(self, request=None, exclude_ids=None):
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:
# No request means no pagination (e.g., homepage)
return {
"title": recommended_page.title if recommended_page else "推薦文章",
"items": ArticlePage.objects.filter(recommended=True).live()[:BLOCK_SIZE],
"url": recommended_page.url if recommended_page else "#",
"title": trending_page.title,
"items": articles_qs[:HORIZON_SIZE],
"url": trending_page.url,
}
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")
try:
@ -104,27 +147,43 @@ class HomePage(Page, CategoryMixin):
def get_context(self, 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()
# 若第一層沒有,抓 descendant CategoryPage
if not categories.exists():
categories = CategoryPage.objects.descendant_of(self).live().in_menu()
for category in categories:
subcategories = category.get_children().type(CategoryPage).live()
category_blocks.append(
sections["category_sections"].append(
{
"title": category.title,
"type": "category",
"items": ArticlePage.objects.child_of(category).live()[:BLOCK_SIZE],
"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
@ -133,16 +192,22 @@ class LatestPage(Page, CategoryMixin):
def get_context(self, 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
class RecommendedPage(Page, CategoryMixin):
class TrendingPage(Page, CategoryMixin):
template = "home/category_page.html"
def get_context(self, 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
@ -153,7 +218,10 @@ class CategoryPage(Page, CategoryMixin):
def get_context(self, 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
@ -163,6 +231,13 @@ from wagtail.images.blocks import ImageChooserBlock
from wagtail.fields import StreamField
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):
cover_image = models.ForeignKey(
@ -194,14 +269,33 @@ class ArticlePage(Page):
],
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 + [
FieldPanel("recommended"),
FieldPanel("trending"),
FieldPanel("cover_image"),
FieldPanel("banner_image"),
FieldPanel("date"),
FieldPanel("intro"),
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">
{{ page.body }}
</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>
{% endblock %}

View File

@ -1,9 +1,31 @@
{% extends "base.html" %}
{% load wagtailcore_tags %}
{% 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 %}
{% include "home/includes/category_block_list.html" %}
{% for section in category_sections %}
{% include "home/includes/category_session.html" with section=section %}
{% endfor %}
{% else %}
{% include "home/includes/category_full_list.html" %}
{% include "home/includes/page-article-list.html" with category=category_sections.0 %}
{% endif %}
{% endblock %}

View File

@ -1,14 +1,20 @@
{% extends "base.html" %}
{% load static %}
{% block body_class %}template-homepage{% endblock %}
{% block extra_css %}
{% endblock extra_css %}
{% 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 %}
{% endblock content %}
{% for section in sections.category_sections %}
{% include "home/includes/category_session.html" with section=section %}
{% 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.messages.context_processors.messages",
"wagtail.contrib.settings.context_processors.settings",
"home.context_processors.navigation_pages",
],
},
},

View File

@ -16,6 +16,21 @@
<ul>
{% with site_root=page.get_site.root_page %}
{# 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 %}
<li>
<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
gunicorn
dj-database-url
psycopg[binary]
psycopg[binary]
python-dotenv