Add sitemap, RSS feed, OG image, and pagination size controls
This commit is contained in:
parent
b16ee811e3
commit
8c4ce7b92e
30
innovedus_cms/home/feeds.py
Normal file
30
innovedus_cms/home/feeds.py
Normal file
@ -0,0 +1,30 @@
|
||||
from django.contrib.syndication.views import Feed
|
||||
from django.utils.feedgenerator import Rss201rev2Feed
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
from .models import ArticlePage
|
||||
|
||||
|
||||
class LatestArticlesFeed(Feed):
|
||||
feed_type = Rss201rev2Feed
|
||||
title = "DeBuT AI 最新文章"
|
||||
link = "/"
|
||||
description = "DeBuT AI 最新文章 RSS feed"
|
||||
|
||||
def items(self):
|
||||
return ArticlePage.objects.live().order_by("-date", "-id")[:20]
|
||||
|
||||
def item_title(self, item):
|
||||
return item.title
|
||||
|
||||
def item_description(self, item):
|
||||
return strip_tags(item.intro or item.search_description or "")
|
||||
|
||||
def item_link(self, item):
|
||||
return item.url
|
||||
|
||||
def item_pubdate(self, item):
|
||||
return item.date
|
||||
|
||||
def item_categories(self, item):
|
||||
return list(item.tags.values_list("name", flat=True))
|
||||
@ -8,6 +8,7 @@ from modelcluster.contrib.taggit import ClusterTaggableManager
|
||||
from modelcluster.fields import ParentalKey
|
||||
from taggit.models import TaggedItemBase
|
||||
from wagtail.search import index
|
||||
from .pagination import build_pagination_context, get_page_size
|
||||
|
||||
def _get_env_int(name, default):
|
||||
value = os.environ.get(name)
|
||||
@ -62,7 +63,7 @@ class CategoryMixin:
|
||||
ArticlePage.objects.child_of(self)
|
||||
.live()
|
||||
.order_by("-date", "-id"),
|
||||
PAGE_SIZE,
|
||||
get_page_size(request, PAGE_SIZE),
|
||||
)
|
||||
page_number = request.GET.get("page") if request else None
|
||||
|
||||
@ -78,7 +79,7 @@ class CategoryMixin:
|
||||
"title": self.title,
|
||||
"items": page_obj,
|
||||
"url": self.url,
|
||||
"page_range": paginator.get_elided_page_range(page_obj.number),
|
||||
"pagination": build_pagination_context(request, page_obj, paginator),
|
||||
}
|
||||
)
|
||||
return blocks
|
||||
@ -98,7 +99,8 @@ class CategoryMixin:
|
||||
else:
|
||||
# Paginated view
|
||||
paginator = Paginator(
|
||||
ArticlePage.objects.live().order_by("-date", "-id"), PAGE_SIZE
|
||||
ArticlePage.objects.live().order_by("-date", "-id"),
|
||||
get_page_size(request, PAGE_SIZE),
|
||||
)
|
||||
page_number = request.GET.get("page")
|
||||
|
||||
@ -112,7 +114,7 @@ class CategoryMixin:
|
||||
"title": self.title,
|
||||
"items": page_obj,
|
||||
"url": self.url,
|
||||
"page_range": paginator.get_elided_page_range(page_obj.number),
|
||||
"pagination": build_pagination_context(request, page_obj, paginator),
|
||||
}
|
||||
|
||||
def get_trending_articles(self, request=None, exclude_ids=None):
|
||||
@ -134,7 +136,7 @@ class CategoryMixin:
|
||||
}
|
||||
else:
|
||||
# Paginated view
|
||||
paginator = Paginator(articles_qs, PAGE_SIZE)
|
||||
paginator = Paginator(articles_qs, get_page_size(request, PAGE_SIZE))
|
||||
page_number = request.GET.get("page")
|
||||
|
||||
try:
|
||||
@ -147,7 +149,7 @@ class CategoryMixin:
|
||||
"title": self.title,
|
||||
"items": page_obj,
|
||||
"url": self.url,
|
||||
"page_range": paginator.get_elided_page_range(page_obj.number),
|
||||
"pagination": build_pagination_context(request, page_obj, paginator),
|
||||
}
|
||||
|
||||
|
||||
@ -310,6 +312,15 @@ class ArticlePage(Page, BreadcrumbMixin):
|
||||
def get_context(self, request):
|
||||
context = super().get_context(request)
|
||||
|
||||
if self.cover_image:
|
||||
cover = self.cover_image.get_rendition("original")
|
||||
context["og_image"] = {
|
||||
"url": request.build_absolute_uri(cover.url),
|
||||
"width": cover.width,
|
||||
"height": cover.height,
|
||||
"alt": self.title,
|
||||
}
|
||||
|
||||
breadcrumbs, site_root = self.build_breadcrumbs()
|
||||
# context["breadcrumbs"] = breadcrumbs
|
||||
# context["breadcrumb_root"] = site_root
|
||||
|
||||
56
innovedus_cms/home/pagination.py
Normal file
56
innovedus_cms/home/pagination.py
Normal file
@ -0,0 +1,56 @@
|
||||
PAGE_SIZE_OPTIONS = (10, 20, 30)
|
||||
|
||||
|
||||
def get_page_size(request, default):
|
||||
try:
|
||||
page_size = int(request.GET.get("page_size", default))
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
return page_size if page_size in PAGE_SIZE_OPTIONS else default
|
||||
|
||||
|
||||
def build_query_string(request, **updates):
|
||||
query = request.GET.copy()
|
||||
|
||||
for key, value in updates.items():
|
||||
if value is None:
|
||||
query.pop(key, None)
|
||||
else:
|
||||
query[key] = value
|
||||
|
||||
return query.urlencode()
|
||||
|
||||
|
||||
def build_pagination_context(request, page_obj, paginator):
|
||||
page_range = paginator.get_elided_page_range(page_obj.number)
|
||||
page_size = paginator.per_page
|
||||
|
||||
return {
|
||||
"page_size": page_size,
|
||||
"page_size_options": [
|
||||
{
|
||||
"value": option,
|
||||
"url": f"?{build_query_string(request, page_size=option, page=None)}",
|
||||
"is_current": option == page_size,
|
||||
}
|
||||
for option in PAGE_SIZE_OPTIONS
|
||||
],
|
||||
"pages": [
|
||||
{
|
||||
"number": page_num,
|
||||
"url": f"?{build_query_string(request, page=page_num)}"
|
||||
if page_num != "…"
|
||||
else "",
|
||||
"is_current": page_num == page_obj.number,
|
||||
"is_ellipsis": page_num == "…",
|
||||
}
|
||||
for page_num in page_range
|
||||
],
|
||||
"previous_url": f"?{build_query_string(request, page=page_obj.previous_page_number())}"
|
||||
if page_obj.has_previous()
|
||||
else "",
|
||||
"next_url": f"?{build_query_string(request, page=page_obj.next_page_number())}"
|
||||
if page_obj.has_next()
|
||||
else "",
|
||||
}
|
||||
@ -136,6 +136,35 @@
|
||||
color: #0e1b4266;
|
||||
}
|
||||
|
||||
.page-size-selector {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 40px 0 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-size-label {
|
||||
color: #0e1b4266;
|
||||
}
|
||||
|
||||
.page-size-option {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 40px;
|
||||
height: 32px;
|
||||
border: 1px solid #0e1b42;
|
||||
border-radius: 4px;
|
||||
color: #0e1b42;
|
||||
}
|
||||
|
||||
.page-size-option.is-current {
|
||||
background: #0e1b42;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@ -3,6 +3,14 @@
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.template-darkbackground .category-title {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.template-darkbackground .category-title span {
|
||||
line-height: 60px;
|
||||
}
|
||||
|
||||
.subcategory-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -63,6 +63,13 @@
|
||||
<path class="icon-cutout" d="M6.321 6.016c-.27-.18-1.166-.802-1.166-.802.756-1.081 1.753-1.502 3.132-1.502.975 0 1.803.327 2.394.948s.928 1.509 1.005 2.644q.492.207.905.484c1.109.745 1.719 1.86 1.719 3.137 0 2.716-2.226 5.075-6.256 5.075C4.594 16 1 13.987 1 7.994 1 2.034 4.482 0 8.044 0 9.69 0 13.55.243 15 5.036l-1.36.353C12.516 1.974 10.163 1.43 8.006 1.43c-3.565 0-5.582 2.171-5.582 6.79 0 4.143 2.254 6.343 5.63 6.343 2.777 0 4.847-1.443 4.847-3.556 0-1.438-1.208-2.127-1.27-2.127-.236 1.234-.868 3.31-3.644 3.31-1.618 0-3.013-1.118-3.013-2.582 0-2.09 1.984-2.847 3.55-2.847.586 0 1.294.04 1.663.114 0-.637-.54-1.728-1.9-1.728-1.25 0-1.566.405-1.967.868ZM8.716 8.19c-2.04 0-2.304.87-2.304 1.416 0 .878 1.043 1.168 1.6 1.168 1.02 0 2.067-.282 2.232-2.423a6.2 6.2 0 0 0-1.528-.161" transform="translate(7.59 7.59) scale(1.125)"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://social-plugins.line.me/lineit/share?url={{ share_url }}" target="_blank" rel="noopener noreferrer" aria-label="分享到 LINE">
|
||||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 33.18 33.18" role="img">
|
||||
<path d="M16.59 0C7.43 0 0 7.43 0 16.59C0 25.75 7.43 33.18 16.59 33.18C25.75 33.18 33.18 25.75 33.18 16.59C33.18 7.43 25.75 0 16.59 0Z" fill="var(--fill-0, #0E1B42)"/>
|
||||
<path class="icon-cutout" d="M17.69 6.71L16.06 6.67L15.12 6.74L14.01 6.9L13.17 7.1L12.17 7.4L11.03 7.87L10.19 8.31L9.45 8.78L8.41 9.59L7.78 10.21L7.23 10.85L6.63 11.73L6.22 12.49L5.86 13.43L5.62 14.44L5.56 15.02L5.54 15.99L5.59 16.56L5.86 17.82L6.38 19.08L6.72 19.68L7.31 20.51L7.74 21.01L8.67 21.89L9.69 22.63L10.61 23.17L11.49 23.57L12.38 23.91L13.41 24.2L15.02 24.5L15.55 24.74L15.76 25L15.84 25.39L15.83 25.89L15.6 27.38L15.67 27.56L15.86 27.69L16.31 27.64L17.09 27.28L19.21 26L20.95 24.82L22.15 23.93L23.3 23.01L23.44 22.83L23.67 22.68L24.25 22.15L25.7 20.61L26.21 19.91L26.72 19.1L26.97 18.58L27.28 17.77L27.48 17.04L27.59 16.14L27.61 15.29L27.49 14.29L27.28 13.45L26.99 12.65L26.62 11.91L26.1 11.1L25.47 10.32L24.82 9.67L23.91 8.93L23.15 8.42L21.53 7.61L20.85 7.36L19.7 7.03L18.55 6.8Z" fill="var(--cutout, #fff)" />
|
||||
<path d="M9.28 13.25L9.17 13.37L9.19 18.37L9.3 18.45L12.54 18.44L12.64 18.34L12.64 17.42L12.56 17.32L10.37 17.3L10.32 17.25L10.3 13.35L10.21 13.25Z M13.46 13.25L13.37 13.38L13.37 18.34L13.5 18.45L14.37 18.45L14.47 18.4L14.52 18.31L14.52 13.4L14.39 13.25Z M15.41 13.27L15.33 13.37L15.33 18.32L15.46 18.45L16.33 18.45L16.48 18.32L16.48 15.44L16.53 15.41L18.76 18.4L18.84 18.45L19.78 18.42L19.85 18.34L19.83 13.33L19.73 13.25L18.83 13.25L18.71 13.35L18.71 16.22L18.66 16.3L16.38 13.27Z M20.72 13.28L20.67 13.35L20.69 18.37L20.8 18.45L24.06 18.42L24.14 18.32L24.14 17.43L24.09 17.34L23.93 17.29L21.87 17.3L21.81 17.22L21.81 16.48L21.86 16.43L24.01 16.43L24.14 16.28L24.12 15.36L24.03 15.28L21.86 15.28L21.81 15.2L21.81 14.45L21.86 14.4L24.01 14.4L24.14 14.26L24.11 13.32L24.03 13.25Z" fill="var(--fill-0, #0E1B42)" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% with tags=page.tags.all %}
|
||||
|
||||
@ -6,10 +6,23 @@
|
||||
<div class="page-article-list">
|
||||
{% include "home/includes/article_list.html" with items=category.items show_hero=show_hero empty_message=empty_message %}
|
||||
|
||||
{% if category.pagination.page_size_options %}
|
||||
<div class="page-size-selector" aria-label="每頁文章數">
|
||||
<span class="page-size-label">每頁</span>
|
||||
{% for option in category.pagination.page_size_options %}
|
||||
{% if option.is_current %}
|
||||
<span class="page-size-option is-current">{{ option.value }}</span>
|
||||
{% else %}
|
||||
<a class="page-size-option" href="{{ option.url }}">{{ option.value }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if category.items.paginator.num_pages > 1 %}
|
||||
<div class="pagination">
|
||||
{% if category.items.has_previous %}
|
||||
<a class="prev-page" href="?page={{ category.items.previous_page_number }}">
|
||||
<a class="prev-page" href="{{ category.pagination.previous_url }}">
|
||||
<button class="left-arrow" type="button" data-dir="left" aria-label="更多文章">
|
||||
<svg class="left-arrow-icon" xmlns="http://www.w3.org/2000/svg" fill="none" overflow="visible" preserveAspectRatio="none" viewBox="0 0 18.1213 33.4142">
|
||||
<path d="M17.4142 0.707107L1.41421 16.7071L17.4142 32.7071" id="Vector 3" stroke="currentColor" stroke-width="2"/>
|
||||
@ -28,18 +41,18 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="pagination-pages">
|
||||
{% for page_num in category.page_range %}
|
||||
{% if page_num == category.items.number %}
|
||||
<span class="pagination-current">{{ page_num }}</span>
|
||||
{% elif page_num == "…" %}
|
||||
<span class="pagination-ellipsis">{{ page_num }}</span>
|
||||
{% for page_item in category.pagination.pages %}
|
||||
{% if page_item.is_current %}
|
||||
<span class="pagination-current">{{ page_item.number }}</span>
|
||||
{% elif page_item.is_ellipsis %}
|
||||
<span class="pagination-ellipsis">{{ page_item.number }}</span>
|
||||
{% else %}
|
||||
<a href="?page={{ page_num }}">{{ page_num }}</a>
|
||||
<a href="{{ page_item.url }}">{{ page_item.number }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if category.items.has_next %}
|
||||
<a class="next-page" href="?page={{ category.items.next_page_number }}">
|
||||
<a class="next-page" href="{{ category.pagination.next_url }}">
|
||||
<span>NEXT</span>
|
||||
<button class="right-arrow" type="button" data-dir="right" aria-label="更多文章">
|
||||
<svg class="right-arrow-icon" xmlns="http://www.w3.org/2000/svg" fill="none" overflow="visible" preserveAspectRatio="none" viewBox="0 0 18.1213 33.4142">
|
||||
|
||||
@ -5,6 +5,7 @@ from taggit.models import Tag
|
||||
from wagtail.models import Site
|
||||
|
||||
from .models import ArticlePage, CATEGORY_HOT_SIZE, PAGE_SIZE
|
||||
from .pagination import build_pagination_context, get_page_size
|
||||
|
||||
|
||||
def hashtag_search(request, slug):
|
||||
@ -15,7 +16,7 @@ def hashtag_search(request, slug):
|
||||
.order_by("-date", "-id")
|
||||
)
|
||||
|
||||
paginator = Paginator(articles, PAGE_SIZE)
|
||||
paginator = Paginator(articles, get_page_size(request, PAGE_SIZE))
|
||||
page_number = request.GET.get("page")
|
||||
|
||||
try:
|
||||
@ -35,7 +36,7 @@ def hashtag_search(request, slug):
|
||||
"title": f"#{tag.name}",
|
||||
"items": page_obj,
|
||||
"url": request.path,
|
||||
"page_range": paginator.get_elided_page_range(page_obj.number),
|
||||
"pagination": build_pagination_context(request, page_obj, paginator),
|
||||
}
|
||||
],
|
||||
"category_trending": (
|
||||
|
||||
@ -123,6 +123,7 @@ INSTALLED_APPS = [
|
||||
"search",
|
||||
"wagtail.contrib.forms",
|
||||
"wagtail.contrib.redirects",
|
||||
"wagtail.contrib.sitemaps",
|
||||
"wagtail.contrib.settings",
|
||||
"wagtail.embeds",
|
||||
"wagtail.sites",
|
||||
@ -142,6 +143,7 @@ INSTALLED_APPS = [
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"django.contrib.sitemaps",
|
||||
"base",
|
||||
]
|
||||
|
||||
|
||||
@ -311,6 +311,14 @@ a {
|
||||
color: #0e1b42;
|
||||
}
|
||||
|
||||
.template-darkbackground .site-hero-band .news-title {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.template-darkbackground .site-hero-band .news-title span {
|
||||
line-height: 60px;
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.site-container {
|
||||
max-width: 640px;
|
||||
|
||||
@ -16,7 +16,14 @@
|
||||
{% if page.search_description %}
|
||||
<meta name="description" content="{{ page.search_description }}" />
|
||||
{% endif %}
|
||||
{% if og_image %}
|
||||
<meta property="og:image" content="{{ og_image.url }}" />
|
||||
<meta property="og:image:width" content="{{ og_image.width }}" />
|
||||
<meta property="og:image:height" content="{{ og_image.height }}" />
|
||||
<meta property="og:image:alt" content="{{ og_image.alt }}" />
|
||||
{% endif %}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="alternate" type="application/rss+xml" title="DeBuT AI 最新文章 RSS" href="{% url 'article_feed' %}">
|
||||
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
|
||||
<link rel="shortcut icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
|
||||
{% if ga4_measurement_id %}
|
||||
|
||||
@ -4,9 +4,11 @@ from django.contrib import admin
|
||||
|
||||
from wagtail.admin import urls as wagtailadmin_urls
|
||||
from wagtail import urls as wagtail_urls
|
||||
from wagtail.contrib.sitemaps.views import sitemap
|
||||
from wagtail.documents import urls as wagtaildocs_urls
|
||||
|
||||
from search import views as search_views
|
||||
from home.feeds import LatestArticlesFeed
|
||||
from home import views as home_views
|
||||
from base import views as base_views
|
||||
|
||||
@ -14,6 +16,8 @@ urlpatterns = [
|
||||
path("django-admin/", admin.site.urls),
|
||||
path("admin/", include(wagtailadmin_urls)),
|
||||
path("documents/", include(wagtaildocs_urls)),
|
||||
path("feed.xml", LatestArticlesFeed(), name="article_feed"),
|
||||
path("sitemap.xml", sitemap, name="sitemap"),
|
||||
path("health", base_views.health_check, name="health_check"),
|
||||
# use <str:slug> so Unicode tag slugs (e.g. 台北美食) still resolve
|
||||
path("tags/<str:slug>/", home_views.hashtag_search, name="hashtag_search"),
|
||||
|
||||
@ -7,6 +7,7 @@ from django.db.models import Q
|
||||
from wagtail.models import Site
|
||||
|
||||
from home.models import ArticlePage, PAGE_SIZE
|
||||
from home.pagination import build_pagination_context, get_page_size
|
||||
|
||||
|
||||
def search(request):
|
||||
@ -26,7 +27,7 @@ def search(request):
|
||||
results_count = primary_qs.count()
|
||||
|
||||
if results_count:
|
||||
paginator = Paginator(primary_qs, PAGE_SIZE)
|
||||
paginator = Paginator(primary_qs, get_page_size(request, PAGE_SIZE))
|
||||
results_page = paginator.get_page(page_number)
|
||||
query_string = urlencode({"query": search_query})
|
||||
category_sections = [
|
||||
@ -34,6 +35,7 @@ def search(request):
|
||||
"title": f"搜尋:{search_query}",
|
||||
"items": results_page,
|
||||
"url": f"{request.path}?{query_string}",
|
||||
"pagination": build_pagination_context(request, results_page, paginator),
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user