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.
This commit is contained in:
Warren Chen 2025-11-10 16:42:15 +09:00
parent a98d36da14
commit 653847df6a
5 changed files with 99 additions and 57 deletions

View File

@ -7,6 +7,7 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from modelcluster.contrib.taggit import ClusterTaggableManager from modelcluster.contrib.taggit import ClusterTaggableManager
from modelcluster.fields import ParentalKey from modelcluster.fields import ParentalKey
from taggit.models import TaggedItemBase from taggit.models import TaggedItemBase
from wagtail.search import index
def _get_env_int(name, default): def _get_env_int(name, default):
value = os.environ.get(name) value = os.environ.get(name)
@ -272,6 +273,12 @@ class ArticlePage(Page):
trending = models.BooleanField("Trending", default=False, help_text="在熱門區塊顯示") trending = models.BooleanField("Trending", default=False, help_text="在熱門區塊顯示")
tags = ClusterTaggableManager(through="home.ArticlePageTag", blank=True) 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 + [ content_panels = Page.content_panels + [
FieldPanel("trending"), FieldPanel("trending"),
FieldPanel("cover_image"), FieldPanel("cover_image"),
@ -299,3 +306,25 @@ class ArticlePage(Page):
context["related_articles"] = related_articles context["related_articles"] = related_articles
return context 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

@ -38,6 +38,7 @@ def hashtag_search(request, slug):
} }
], ],
"site_root": site_root, "site_root": site_root,
"page": site_root.specific if site_root else None,
} }
return render(request, "home/hashtag_page.html", context) return render(request, "home/hashtag_page.html", context)

View File

@ -56,5 +56,15 @@
{% endif %} {% endif %}
</ul> </ul>
</nav> </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> </div>
</header> </header>

View File

@ -1,38 +1,38 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static wagtailcore_tags %} {% load wagtailcore_tags %}
{% block body_class %}template-searchresults{% endblock %} {% block body_class %}template-searchresults{% endblock %}
{% block title %}Search{% endblock %} {% block title %}
{% if search_query %}搜尋:{{ search_query }}{% else %}搜尋{% endif %}
{% endblock %}
{% block content %} {% block content %}
<h1>Search</h1> <nav class="breadcrumbs" aria-label="breadcrumb">
<ol>
<form action="{% url 'search' %}" method="get"> <li>
<input type="text" name="query"{% if search_query %} value="{{ search_query }}"{% endif %}> {% if site_root %}
<input type="submit" value="Search" class="button"> <a href="{{ site_root.url }}">首頁</a>
</form> {% else %}
<a href="/">首頁</a>
{% 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 }}
{% endif %} {% endif %}
</li> </li>
{% endfor %} <li><span>搜尋</span></li>
</ul> {% if search_query %}
<li><span>{{ search_query }}</span></li>
{% endif %}
</ol>
</nav>
{% if search_results.has_previous %} <section class="search-results">
<a href="{% url 'search' %}?query={{ search_query|urlencode }}&amp;page={{ search_results.previous_page_number }}">Previous</a> {% if search_query %}
{% endif %} {% if results_count %}
{% include "home/includes/page-article-list.html" %}
{% if search_results.has_next %} {% else %}
<a href="{% url 'search' %}?query={{ search_query|urlencode }}&amp;page={{ search_results.next_page_number }}">Next</a> <p>找不到與「{{ search_query }}」相關的文章。</p>
{% endif %} {% endif %}
{% elif search_query %} {% else %}
No results found <p>請輸入關鍵字後再進行搜尋。</p>
{% endif %} {% endif %}
</section>
{% endblock %} {% 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 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 from home.models import ArticlePage, PAGE_SIZE
# <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
def search(request): def search(request):
search_query = request.GET.get("query", None) search_query = (request.GET.get("query") or "").strip()
page = request.GET.get("page", 1) page_number = request.GET.get("page", 1)
category_sections = []
results_page = None
results_count = 0
# Search
if search_query: 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) site = Site.find_for_request(request)
# query.add_hit() site_root = site.root_page if site else None
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)
return TemplateResponse( return TemplateResponse(
request, request,
"search/search.html", "search/search.html",
{ {
"search_query": search_query, "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,
}, },
) )