NARITO BLOG

Django、記事の関連記事を作成する

Python, Django,

概要

ブログではよく、「関連記事」のようなリンクがあります。このブログにもありますね。

関連記事のリンク

今回はこれを実装するにあたって、2つの方法を紹介します。

models.py

次のようなモデルを例に考えていきます。タイトルと本文を持つ記事のモデルです。

from django.db import models


class Post(models.Model):
    title = models.CharField('記事タイトル', max_length=255)
    text = models.TextField('記事本文')

    def __str__(self):
        return self.title

base.html

Bulmaのスターターテンプレートです。JavaScriptを使って配置する方法も紹介するので、{% block extra_js %}としてjsの追加をしやすくしておきます。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Hello Bulma!</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css">
    <script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
</head>
<body>
{% block content %}{% endblock %}
{% block extra_js %}{% endblock %}
</body>
</html>

post_detail.html

記事詳細を左側に、関連記事欄を右側に配置しておきます。JavaScriptを使って配置する方法も紹介するので、関連記事にはid="relation-posts"としておきます。

{% extends 'app/base.html' %}

{% block content %}
    <section class="section">
        <div class="container">
            <div class="columns">
                <article class="column is-9 content">
                    <h1 class="title">{{ post.title }}</h1>
                    {{ post.text | safe | linebreaks }}
                </article>
                <aside class="column is-3" id="relation-posts">
                    <h2 class="title">関連記事</h2>
                </aside>
            </div>

        </div>
    </section>
{% endblock %}

ビューは単純なgeneric.DetailViewを使っておきます。

ここまでで、見た目は次のような感じになります。

最初の見た目

JavaScriptを使う方法

まずはJavaScriptを使った関連リンクの作成方法です。記事の本文中にあるリンク...今回ならば「WindowsでのPythonインストール方法」と「MacでのPythonインストール」というリンクがありますが、これを基に関連記事リンクを作成します。

{% block extra_js %}
    <script>
        let isFound = false;  // 関連記事の作成
        const pathList = new Set();  // URLのpath部分が詰まったセット
        const ulElement = document.createElement('ul');

        // 記事中のa要素を1つずつ取り出す
        for (const a of document.querySelectorAll('article.content a')) {
            const url = new URL(a.href);
            // ドメインは同じだが、パス部分がこの記事と違っていて、まだ追加していないa要素を関連記事として登録
            if (url.hostname === document.domain && url.pathname !== location.pathname && !pathList.has(url.pathname)) {
                const liElement = document.createElement('li');
                liElement.appendChild(a.cloneNode(true));
                ulElement.appendChild(liElement);
                pathList.add(url.pathname);
                isFound = true;
            }
        }
        if (isFound) {
            document.getElementById('relation-posts').appendChild(ulElement);
        }
    </script>
{% endblock %}

次のような感じで、ちゃんと登録されますね。

関連記事は、次のようなルールで登録しています。

  1. url.hostname === document.domain ドメインが同じならば。自分のサイト内のリンクだけ登録したい。
  2. url.pathname !== location.pathname パス部分がこのページと違うならば。ページ内リンクは関連記事にしたくないため。
  3. !pathList.has(url.pathname) まだ追加していないURLのパスならば。 全く同じリンクを関連記事欄に追加したくないため

この辺のルールは好きに変更してください。

ManyToManyFieldを使う方法

モデルを次のように変更しておきます。

class Post(models.Model):
    title = models.CharField('記事タイトル', max_length=255)
    text = models.TextField('記事本文')
    relation_posts = models.ManyToManyField('self', verbose_name='関連記事', blank=True)

    def __str__(self):
        return self.title

relation_posts = models.ManyToManyField('self'の部分が重要です。Djangoで、コメント・返信を再帰的に取り出すでもこのselfを使いました。

これにより、管理画面では次のように関連記事が指定できます。

関連記事を指定する様子

記事数が多くなってくると、関連記事の指定がちょっと面倒になってきます。100記事ある選択欄の中から、関連しそうな記事を手動で選ぶのはつらいものがあります。

その場合はDjangoで、タグのサジェスト機能つきフォームでやったように、関連記事をサジェストして登録できるようにしておくと捗ります。

post_detail.htmlでは、次のように関連記事を取り出せます。

                <aside class="column is-3" id="relation-posts">
                    <h2 class="title">関連記事</h2>
                    {% for rel_post in post.relation_posts.all %}
                         <a href="{% url 'app:post_detail' rel_post.pk %}">{{ rel_post.title }}</a>
                    {% endfor %}
                </aside>