Compare commits

..

2 Commits

Author SHA1 Message Date
Warren Chen
653847df6a Add search functionality to ArticlePage and enhance search templates
- Implement search fields in ArticlePage model for indexing.
- Update hashtag search view to include site root in context.
- Enhance header with a search form for articles.
- Modify search results template to improve user experience and display.
2025-11-10 16:42:15 +09:00
Warren Chen
a98d36da14 Add hashtag search functionality and create hashtag page template 2025-11-10 15:39:43 +09:00
8 changed files with 165 additions and 58 deletions

View File

@ -7,6 +7,7 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from modelcluster.contrib.taggit import ClusterTaggableManager
from modelcluster.fields import ParentalKey
from taggit.models import TaggedItemBase
from wagtail.search import index
def _get_env_int(name, default):
value = os.environ.get(name)
@ -272,6 +273,12 @@ class ArticlePage(Page):
trending = models.BooleanField("Trending", default=False, help_text="在熱門區塊顯示")
tags = ClusterTaggableManager(through="home.ArticlePageTag", blank=True)
search_fields = Page.search_fields + [
index.SearchField("intro", partial_match=True),
index.SearchField("body_search_text", partial_match=True),
index.SearchField("tag_names_search_text", partial_match=True),
]
content_panels = Page.content_panels + [
FieldPanel("trending"),
FieldPanel("cover_image"),
@ -299,3 +306,25 @@ class ArticlePage(Page):
context["related_articles"] = related_articles
return context
@property
def body_search_text(self):
if not self.body:
return ""
excluded_types = {"image", "embed", "hr", "html"}
chunks = []
for block in self.body:
if block.block_type in excluded_types:
continue
# Each block decides how to expose searchable text
block_content = block.block.get_searchable_content(block.value)
if block_content:
chunks.extend(block_content)
return " ".join(text for text in chunks if isinstance(text, str))
@property
def tag_names_search_text(self):
return " ".join(self.tags.values_list("name", flat=True))

View File

@ -19,7 +19,7 @@
<span>Hashtags:</span>
<ul>
{% for tag in tags %}
<li>#{{ tag }}</li>
<li><a href="{% url 'hashtag_search' tag.slug %}">#{{ tag }}</a></li>
{% endfor %}
</ul>
</div>

View File

@ -0,0 +1,19 @@
{% extends "base.html" %}
{% load wagtailcore_tags %}
{% block content %}
<nav class="breadcrumbs" aria-label="breadcrumb">
<ol>
<li>
{% if site_root %}
<a href="{{ site_root.url }}">首頁</a>
{% else %}
<a href="/">首頁</a>
{% endif %}
</li>
<li><span>標籤</span></li>
<li><span>#{{ tag.name }}</span></li>
</ol>
</nav>
{% include "home/includes/page-article-list.html" %}
{% endblock %}

View File

@ -0,0 +1,44 @@
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.shortcuts import get_object_or_404, render
from taggit.models import Tag
from wagtail.models import Site
from .models import ArticlePage, PAGE_SIZE
def hashtag_search(request, slug):
tag = get_object_or_404(Tag, slug=slug)
articles = (
ArticlePage.objects.live()
.filter(tags__slug=slug)
.order_by("-date")
)
paginator = Paginator(articles, PAGE_SIZE)
page_number = request.GET.get("page")
try:
page_obj = paginator.page(page_number)
except PageNotAnInteger:
page_obj = paginator.page(1)
except EmptyPage:
page_obj = paginator.page(paginator.num_pages)
site = Site.find_for_request(request)
site_root = site.root_page if site else None
context = {
"tag": tag,
"category_sections": [
{
"title": f"#{tag.name}",
"items": page_obj,
"url": request.path,
}
],
"site_root": site_root,
"page": site_root.specific if site_root else None,
}
return render(request, "home/hashtag_page.html", context)

View File

@ -56,5 +56,15 @@
{% endif %}
</ul>
</nav>
<form class="header-search" action="{% url 'search' %}" method="get" role="search">
<input
type="search"
name="query"
placeholder="搜尋文章"
value="{{ request.GET.query|default:'' }}"
aria-label="搜尋文章">
<button type="submit">搜尋</button>
</form>
</div>
</header>

View File

@ -7,11 +7,14 @@ from wagtail import urls as wagtail_urls
from wagtail.documents import urls as wagtaildocs_urls
from search import views as search_views
from home import views as home_views
urlpatterns = [
path("django-admin/", admin.site.urls),
path("admin/", include(wagtailadmin_urls)),
path("documents/", include(wagtaildocs_urls)),
# use <str:slug> so Unicode tag slugs (e.g. 台北美食) still resolve
path("tags/<str:slug>/", home_views.hashtag_search, name="hashtag_search"),
path("search/", search_views.search, name="search"),
]

View File

@ -1,38 +1,38 @@
{% extends "base.html" %}
{% load static wagtailcore_tags %}
{% load wagtailcore_tags %}
{% block body_class %}template-searchresults{% endblock %}
{% block title %}Search{% endblock %}
{% block title %}
{% if search_query %}搜尋:{{ search_query }}{% else %}搜尋{% endif %}
{% endblock %}
{% block content %}
<h1>Search</h1>
<form action="{% url 'search' %}" method="get">
<input type="text" name="query"{% if search_query %} value="{{ search_query }}"{% endif %}>
<input type="submit" value="Search" class="button">
</form>
{% if search_results %}
<ul>
{% for result in search_results %}
<li>
<h4><a href="{% pageurl result %}">{{ result }}</a></h4>
{% if result.search_description %}
{{ result.search_description }}
<nav class="breadcrumbs" aria-label="breadcrumb">
<ol>
<li>
{% if site_root %}
<a href="{{ site_root.url }}">首頁</a>
{% else %}
<a href="/">首頁</a>
{% endif %}
</li>
{% endfor %}
</ul>
</li>
<li><span>搜尋</span></li>
{% if search_query %}
<li><span>{{ search_query }}</span></li>
{% endif %}
</ol>
</nav>
{% if search_results.has_previous %}
<a href="{% url 'search' %}?query={{ search_query|urlencode }}&amp;page={{ search_results.previous_page_number }}">Previous</a>
{% endif %}
{% if search_results.has_next %}
<a href="{% url 'search' %}?query={{ search_query|urlencode }}&amp;page={{ search_results.next_page_number }}">Next</a>
{% endif %}
{% elif search_query %}
No results found
{% endif %}
<section class="search-results">
{% if search_query %}
{% if results_count %}
{% include "home/includes/page-article-list.html" %}
{% else %}
<p>找不到與「{{ search_query }}」相關的文章。</p>
{% endif %}
{% else %}
<p>請輸入關鍵字後再進行搜尋。</p>
{% endif %}
</section>
{% endblock %}

View File

@ -1,46 +1,48 @@
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from urllib.parse import urlencode
from django.core.paginator import Paginator
from django.template.response import TemplateResponse
from wagtail.models import Page
from wagtail.models import Site
# To enable logging of search queries for use with the "Promoted search results" module
# <https://docs.wagtail.org/en/stable/reference/contrib/searchpromotions.html>
# uncomment the following line and the lines indicated in the search function
# (after adding wagtail.contrib.search_promotions to INSTALLED_APPS):
# from wagtail.contrib.search_promotions.models import Query
from home.models import ArticlePage, PAGE_SIZE
def search(request):
search_query = request.GET.get("query", None)
page = request.GET.get("page", 1)
search_query = (request.GET.get("query") or "").strip()
page_number = request.GET.get("page", 1)
category_sections = []
results_page = None
results_count = 0
# Search
if search_query:
search_results = Page.objects.live().search(search_query)
search_queryset = ArticlePage.objects.live().search(search_query)
paginator = Paginator(search_queryset, PAGE_SIZE)
results_page = paginator.get_page(page_number)
results_count = paginator.count
# To log this query for use with the "Promoted search results" module:
if results_count:
query_string = urlencode({"query": search_query})
category_sections = [
{
"title": f"搜尋:{search_query}",
"items": results_page,
"url": f"{request.path}?{query_string}",
}
]
# query = Query.get(search_query)
# query.add_hit()
else:
search_results = Page.objects.none()
# Pagination
paginator = Paginator(search_results, 10)
try:
search_results = paginator.page(page)
except PageNotAnInteger:
search_results = paginator.page(1)
except EmptyPage:
search_results = paginator.page(paginator.num_pages)
site = Site.find_for_request(request)
site_root = site.root_page if site else None
return TemplateResponse(
request,
"search/search.html",
{
"search_query": search_query,
"search_results": search_results,
"category_sections": category_sections,
"results_page": results_page,
"results_count": results_count,
"site_root": site_root,
"page": site_root.specific if site_root else None,
},
)