Python、Django でブログ実装(こつこつ④検索、ページネーション実装、JavaScript で便利な機能)
今回は、検索機能やページネーションを実装し、それから JavaScript を使って、少し便利な機能を実装しよーと思います!
それでは、いきまっしょい♪
前回までの記事
前回 2回前 3回前
前回までのプロジェクト(コード)
GitHub: https://github.com/Animalyzm/mikoto_project
今回のプロジェクトは、django/blog です。
Git のコミット・メッセージ
前回: django_blog_3_comment
2回前: django_blog_2_bugfix
3回前: django_blog_1_markdown
今回やること
検索機能とページネーションを実装し、JavaScript でトップ・スクロール・ボタンと、記事のもくじリンクを実装します。
環境
Windows10、PyCharm、Python v3.13.2、Django v5.1.7
ディレクトリ構成
※これまでに変更・作成したファイルのみになります。
blog
├ media(以下、省略)
├ static(以下、省略)
├ templates(以下、省略)
├ blog
| ├ settings.py
| └ urls.py
└ app
├ templatetags/mytag.py
├ admin.py
├ context_processors.py.py
├ models.py
├ urls.py
└ views.py
検索機能と検索結果にも機能するページネーションを実装する
ページネーションは、Udemy のレクチャーを参考にさせてもらいました。(後述)
views.py、templatetags/mytag.py、HTML の順に作成していきます。
ページ毎の記事件数は、paginate_by で設定し、検索用メソッド、カテゴリ・リンク用のメソッドを実装します。
...
class IndexView(generic.ListView):
model = Post
paginate_by = 5
def get_queryset(self):
queryset = Post.objects.order_by('-created_at') # 降順
keyword = self.request.GET.get('keyword') # 検索入力キーワード
if keyword:
queryset = queryset.filter(
Q(title__icontains=keyword) | Q(content__icontains=keyword)
) # icontains: 部分一致、大小文字区別なし
return queryset
...
class CategoryView(generic.ListView):
model = Post
paginate_by = 5
def get_queryset(self):
category_pk = self.kwargs['category_pk']
queryset = Post.objects.order_by('-created_at').filter(category__pk=category_pk)
return queryset
...
テンプレート・タグを使い、検索時などでもページネーションが機能するように、URL を書き換える関数を実装します。
simple-tags: https://docs.djangoproject.com/ja/5.2/howto/custom-template-tags/#simple-tags
from django import template
register = template.Library() # タグやフィルター登録の初期化処理
@register.simple_tag
def url_replace(request, key, value):
url_dict = request.GET.copy()
print(url_dict, key, value)
url_dict[key] = value
return url_dict.urlencode()
ページネーションのレイアウトは、ブートストラップを使います。
{% load mytag %}
<nav aria-label="Page navigation">
<ul class="pagination fw-bold">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link bg-dark text-secondary page_border" href="?{% url_replace request 'page' page_obj.previous_page_number %}" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
{% endif %}
{% if page_obj.number > 3 %}
<li class="page-item">
<a class="page-link bg-dark text-secondary page_border" href="?page=1" aria-label="First">
1
</a>
</li>
{% endif %}
{% if page_obj.number > 4 %}
<li class="page-item">
<span class="page-link bg-dark text-secondary page_border" aria-hidden="true">
...
</span>
</li>
{% endif %}
{% for link_page in page_obj.paginator.page_range %}
{% if link_page == page_obj.number %}
<li class="page-item">
<a class="page-link bg-dark text-light page_border" href="?{% url_replace request 'page' link_page %}">
{{ link_page }}
</a>
</li>
{% elif link_page < page_obj.number|add:3 and link_page > page_obj.number|add:-3 %}
<li class="page-item">
<a class="page-link bg-dark text-secondary page_border" href="?{% url_replace request 'page' link_page %}">
{{ link_page }}
</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.number < page_obj.paginator.num_pages|add:-3 %}
<li class="page-item">
<span class="page-link bg-dark text-secondary page_border" aria-hidden="true">
...
</span>
</li>
{% endif %}
{% if page_obj.number < page_obj.paginator.num_pages|add:-2 %}
<li class="page-item">
<a class="page-link bg-dark text-secondary page_border" href="?page={{ page_obj.paginator.num_pages }}" aria-label="Last">
{{ page_obj.paginator.num_pages}}
</a>
</li>
{% endif%}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link bg-dark text-secondary page_border" href="?{% url_replace request 'page' page_obj.next_page_number %}" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
{% endif %}
</ul>
</nav>
リストページに、スニペットをはります。
...
{% endfor %}
<!-- ページネーション -->
<span class="d-flex justify-content-center">
{% include 'snippets/page.html' %}
</span>
</div>
<!-- 768ピクセル以上 -->
...
検索ボックスのレイアウトにも、ブートストラップを使います。
...
{% endif %}
</ul>
<!-- 検索 -->
<form class="d-flex" method="GET" action="{% url 'app:index' %}">
<input class="form-control fw-bold ms-3" type="search" placeholder="Search" aria-label="Search" name="keyword" style="background-color:#111; color: #ccc;">
<button class="btn btn-outline-success mx-2 fw-bold" type="submit">Search</button>
</form>
</div>
</div>
</nav>
...
CSS でページネーションの表示を微調整します。
...
.page_border {
border: 1px solid #555;
}
...
動作確認します。
とりあえず、カテゴリ別、検索ワード別の記事をたくさん書きます。
ページネーションの確認をするので、1ページあたりの表示件数を2に減らします。
まず、通常のホーム画面からのページネーションを確認します。
検索ボックスと、ページネーションの表示は、問題なく表示されています。

中間は省略しますが、5ページ目まで無事に表示されました。

カテゴリ・リンクをテストします。
それぞれのカテゴリを全ページ表示させた結果、ちゃんとカテゴリ別でページネーションが機能しました。
(画像はさぶ・カテゴリの最終ページです)

次は、検索でページネーションを試します。
「あにまる」を検索した結果、ちゃんと、ページネーションが機能しました。
(画像は検索結果の最終ページです)

トップ・スクロール・ボタンを実装する(JavaScript)
HTML(JavaScript)、CSS の順にファイルを作ります。
...
</main>
<!-- トップへ戻るボタン -->
<div id="js-scroll-top" class="scroll-top">⇧</div>
<!-- フッター -->
...
<script>
// スクロールボタン
const PageTopBtn = document.getElementById('js-scroll-top');
PageTopBtn.addEventListener('click', () =>{
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
//スクロール時のイベントを追加
window.addEventListener('scroll' , scroll_event );
function scroll_event(){
if(window.pageYOffset > 50){
PageTopBtn.style.opacity = '0.5';
// ホバー時設定
PageTopBtn.addEventListener('mouseover', function() {
PageTopBtn.style.opacity = 1;
PageTopBtn.style.cursor = "pointer";
});
PageTopBtn.addEventListener('mouseleave', function() {
PageTopBtn.style.opacity = 0.5;
});
}else if(window.pageYOffset < 50){
PageTopBtn.style.opacity = '0';
// ホバー時設定
PageTopBtn.addEventListener('mouseover', function() {
PageTopBtn.style.opacity = 0;
PageTopBtn.style.cursor = "default";
});
PageTopBtn.addEventListener('mouseleave', function() {
PageTopBtn.style.opacity = 0;
});
}
};
</script>
スクロール・ボタンの表示を CSS で調整します。
...
/* スクロールボタン */
.scroll-top {
position: fixed;
right: 15px;
bottom: 25px;
z-index: 100;
background-color: #666;
width: 50px;
height: 50px;
border-radius: 20%;
color: #fff;
line-height: 50px;
text-align: center;
font-size: 30px;
opacity: 0;
transition-duration: 0.5s;
}
/* 自作 */
動作確認します。
ページネーションの記事数を5に戻して、ホーム画面(http://localhost:8080/app/)にアクセスします。
初期画面では矢印は表示されてません。

下方の画面をスクロールさせると、右下にトップ・スクロール・ボタンが表示されます。

トップ・スクロール・ボタンをクリックすると、トップまで戻ってボタンは消えます。

スマホでも使える TOC もくじリンクを作る(JavaScript)
すでにマークダウンを使えるようにしてあり、extensions で TOC も使えるようになっています。
まず、もくじつきの記事を書いていきます。
とりあえず、もくじは最上部に表示させ、それをナビバーへと移動させます。
もくじを表示させるには、記事の表示させたい場所に [TOC] と書くだけです。
長めの記事にするため、改行を多めに入れています。

詳細ページにアクセスすると、こんな感じの表示になります。

最初にもくじリンクがあるので、「アルファベット」をクリックすると、アルファベットのところまでスクロールします。

このままだと、最上部にいるときにしか使えないので、ナビバーに移し、どこでも、スマホでもリンクを使えるようにします。
HTML(JavaScript)、CSS の順にファイルを作ります。
詳細記事ページでのみ、もくじリンクが表示されるようにします。
...
<!-- 管理者のみ表示 -->
{% if user.is_superuser %}
<li class="nav-item ms-3">
<a class="nav-link" href="{% url 'admin:index' %}">Admin</a>
</li>
{% endif %}
<!-- TOC -->
{% if 'detail' in request.resolver_match.url_name %}
<li class="nav-item ms-3 toc-display" style="border-bottom: none;">
<span class="toc-display">
<a class="nav-link active fw-bold">もくじ</a>
<span id="toc" class="card"></span>
</span>
</li>
{% endif %}
</ul>
...
<script>
// TOC の位置を貼りかえる(本文からナビバーへ)
const toc = document.querySelector('div.card-body > div.toc > ul');
console.log(toc);
if (toc !== null) {
const copyToc = toc.cloneNode(true);
document.querySelector('span#toc').appendChild(copyToc);
document.querySelector('div.card-body > div.toc').removeChild(toc);
}
// スクロールボタン
...
ホバー時に、もくじリンクが表示されるように、CSS で調整します。
...
/* TOC: ホバーで目次表示 */
#toc {
border: none;
}
#toc ul {
display: none;
position: absolute;
width: 300px;
list-style: none;
background-color: #000;
padding: 1rem;
border-radius: 0.5em;
font-weight: bold;
}
.toc-display {
position: relative;
display: inline-block;
}
.toc-display:hover #toc ul {
display: block;
opacity: 0.8;
border-bottom: none;
}
/* スクロールボタン */
...
動作確認します。
詳細ページにアクセスすると、ナビバーに「もくじ」が表示されます。

マウスをもくじのところへ持っていくと、もくじリンクが表示されます。

「アルファベット」をクリックすると、ちゃんとスクロールし、もくじリンクもナビバーに表示されます。

若干見にくいですが、スマホ表示に変更しても、目次リンクは表示されます。

今回のプロジェクト(コード)
GitHub: https://github.com/Animalyzm/mikoto_project
今回のプロジェクトは、django/blog です。
Git のコミット・メッセージは、django_blog_4_search_pagination です。
データベースなどは削除してるので、使用するにはマイグレートが必要になります。
python manage.py makemigrations
python manage.py migrate
参考にさせてもらったレクチャー
滝澤成人(最終更新月2022年7月)、プログラミング初心者でも安心、Python/Django入門講座、Udemy レクチャー
4回にわたり記事を書きましたが、やっとブログが完成しました!
なかなか、使いやすいブログになったのではないかと思います。
今回は以上になります、ありがとうございましたー♪
Discussion