🌏

Python、Django でブログ実装(こつこつ② HTML ファイルをつくり、カテゴリを追加する)

に公開

今日は、前回の続きをやろーと思います!


前回の記事

https://zenn.dev/animalz/articles/30f1cd844a57bf

前回のプロジェクト(コード)

GitHub: https://github.com/Animalyzm/mikoto_project
今回のプロジェクトは、django/blog です。
(Git のコミット・メッセージは django_blog_1_markdown です)


今回やること

記事一覧ページと詳細ページを作り、カテゴリを追加します。


環境

エディタ以外は前回同様です。(前回は、書き忘れてました)
Windows10、PyCharm、Python v3.13.2、Django v5.1.7
(VACode で仮想環境を使えなくなったため、急遽、PyCharm に変更しました)


ディレクトリ構成

※これまでに変更・作成したファイルのみになります。

ディレクトリ構成
blog
├ media
├ static(以下、省略)
├ templates(以下、省略)
├ blog
| ├ settings.py
| └ urls.py
└ app
  ├ admin.py
  ├ context_processors.py.py
  ├ models.py
  ├ urls.py
  └ views.py

記事一覧ページを作成する

前回、URL とビューは作成しているので、HTML ファイルを作成します。
まず、ベースのファイルを作成します。
レイアウトは、ブートストラップ・ベースで CSS で微調整していきます。
ブートストラップ: https://getbootstrap.jp/docs/5.0/getting-started/introduction

base.html
<!doctype html>
<html lang="ja">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- Google Fonts --> 
    <link rel="preconnect" href="https://fonts.gstatic.com">
    <link href="https://fonts.googleapis.com/css2?family=M+PLUS+1p:wght@300&display=swap" rel="stylesheet">

    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">

    <!-- CSSファイルの読み込み -->
    {% load static %}
    <link rel="stylesheet" type='text/css' href="{% static 'app/style.css' %}">

    <title>ブログ</title>
  </head>
  
  <body>
    <header class="fixed-top">
      <nav class="navbar navbar-expand-lg navbar-dark p-2">
        <div class="container-fluid">
          <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
          </button>
          <div class="collapse navbar-collapse rad_5" id="navbarSupportedContent">
            <ul class="navbar-nav me-auto mb-2">
              <li class="nav-item ms-3">
                <a class="nav-link active fw-bold" aria-current="page" href="{% url 'app:index' %}">Home</a>
              </li>
              <!-- 管理者のみ表示 -->
              {% if user.is_superuser %}
                <li class="nav-item ms-3">
                  <a class="nav-link" href="{% url 'admin:index' %}">Admin</a>
                </li>
              {% endif %}
            </ul>
          </div>
        </div>
      </nav>
    </header>

    <main>    
      {% block content %}
      {% endblock content %}
    </main>
    
    <!-- フッター -->
    <footer class="text-light text-center py-3">
      <hr>
      © 2025
    </footer>

    <!-- Option 1: Bootstrap Bundle with Popper -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script>
  </body>
</html>

レスポンシブ・デザイン(PC でもスマホでもちゃんと表示される)を実現するために、2カラム・レイアウトを使います。

post_list.html
{% extends 'app/base.html' %}
{% block content %}
<div class="container">
  <div class="mb-3 p-3 bg_fc_dark rad_5" style="margin-top: 5rem;">
    <h1 class="fw-bold">ブログ</h1>
    <hr>
    説明
  </div>
  <!-- 2カラムレイアウト -->
  <div class="row">
    <div class="col-md-8">
      {% for post in post_list %}
        <div class="card mb-2 bg_fc_dark rad_5">
          <div class="card-body pb-0">
            <p class="fs-6">{{post.created_at }}, {{ post.category }}</p>
            <p class="fw-bold fs-5">
              <span class="me-3">{{ post.title }}</span>
              <a href="#" class="btn btn-dark btn-sm">続きを読む</a>
            </p>
          </div>
        </div>
      {% endfor %}

    </div>
    <!-- 768ピクセル以上 -->
    <div class="col-md-4">
    </div>
  </div>
</div>
{% endblock %}

背景色や文字色、Web フォントの適用や、初期フッター位置の調整、その他の調整を CSS で行います。

static/app/style.css
html {
  background-color: #222;
}
body { 
  /* Web フォント */ 
  font-family: "M PLUS 1p", sans-serif;
  background-color: #222;
  color: #ccc;
  /* 初期のフッター位置調整 */
  display: flex; 
  flex-flow: column; 
  min-height: 100vh; 
}
main {
  /* 初期のフッター位置調整 */
  flex: 1; 
}
a {
  text-decoration: none;
  color: aquamarine;
}
a:visited {
  color: aquamarine;
}
a:hover {
  color: lightgreen;
}
/* 自作 */
.bg_fc_dark {
  background-color: #111;
  color: #ccc;
}
.pos_right {
  float: right;
}
.rad_5 {
  border-radius: 0.5em;
}

マイグレート~スーパー・ユーザー作成

毎回、データベースを削除した状態から始めるのと、マイグレートと、スーパー・ユーザー作成を行います。

コマンド・プロンプト
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser

動作確認

コマンド・プロンプト
python manage.py runserver 8080

http://localhost:8080/admin/にアクセスして、テスト記事を書きます。(ダーク・モードになります)

http://localhost:8080/app/ にアクセスして、画面を確認します。
無事に表示されました。


詳細ページを作る

詳細ページは、urls.py、views.py、HTML の順に作成します。
パスを指定します。

app/urls.py
...
urlpatterns = [
    ...
    path('detail/<int:pk>/', views.DetailView.as_view(), name='detail'),
]

ビューとビュー・カウントを実装します。

app/views.py
...
class DetailView(generic.DetailView):
    model = Post
    
    def get(self, request, *args, **kwargs):
        self.object = self.get_object()
        self.object.page_views += 1
        self.object.save() 
        context = self.get_context_data(object=self.object)
        return self.render_to_response(context)

HTML ファイルを作っていきます。
記事本文は、HTML エスケープを safe フィルタで無効にして、マークダウンを HTML に変換して表示します。

post_detail.html
{% extends 'app/base.html' %}
{% block content %}
<div class="container">
  <div class="row" style="margin-top: 5rem;">
    <div class="col-md-8 pb-3">
      <div class="card mb-1 bg_fc_dark rad_5">
        <div class="card-body">
          <span class="fs-6">{{post.created_at }}, {{ post.category }}</span>
          <p class="fs-4 fw-bold">{{ post.title }}</p>
          <hr>
          <!-- safe フィルタで HTML エスケープを無効にする -->
          {{ post.markdown_to_html | safe }}
        </div>
      </div>
      <span class="pe-2 pos_right">PV: {{ post.page_views }}</span><br>

      <a class="btn m-1 bg_fc_dark pos_right" href="{% url 'app:index' %}">ホーム</a>
      <hr>
    </div>
    <!-- 768ピクセル以上 -->
    <div class="col-md-4">
    </div>
  </div>
</div>
{% endblock %}

リスト・ページに、詳細ページのリンクをはります。

post_list.html
...
              <a href="{% url 'app:detail' post.pk %}" class="btn btn-dark btn-sm">続きを読む</a>
...

動作確認

サーバーを立ち上げます

コマンド・プロンプト
python manage.py runserver 8080

http://localhost:8080/app にアクセスします。

続きを読むリンクをクリックします。
無事に記事詳細ページが表示され、ビュー・カウントも機能しているようです。


カテゴリを作成する

カテゴリは、サイドバー(2カラム目)に表示させるため、コンテキスト・プロセッサーを使い、スニペットとして HTML を作成します。
ちょっと、ややこしいですが、urls.py、models.py、context_proccessors.py、settings.py、views.py、HTML、CSS、admin.py の順にファイルを作成していきます。

URL パスを指定します。

app/urls.py
...
urlpatterns = [
    ...
    path('category/<int:category_pk>/', views.CategoryView.as_view(), name='category'),
]

カテゴリー・モデルを作成し、ポスト・モデルに紐づけます。

models.py
...
class Category(models.Model):
    category_name = models.CharField('カテゴリ名', max_length=32)
    created_at = models.DateTimeField('作成日', auto_now_add=True)
    
    def __str__(self):
        return self.category_name

class Post(models.Model):
    ...
    page_views = models.PositiveBigIntegerField(default=0)
    category = models.ForeignKey(Category, verbose_name='カテゴリ', blank=True, null=True, on_delete=models.SET_NULL)  # 追加
...

コンテキスト・プロセッサー用のファイルを作成し、設定ファイルの登録します。

context_procesoors.py
from .models import Category

def category_list(request):
    """ テンプレートに毎回渡すデータ
    settings.py: TEMPLATES...OPTIONSの設定必要
    """
    context = {
        'category_list': Category.objects.all(),
    }
    return context
settings.py
...
TEMPLATES = [
    {
        ...
            'context_processors': [
                ...
                'app.context_processors.category_list',  # カテゴリ一覧表示用 # 追加
...

ビューを作成します。

app/views.py
...
class CategoryView(generic.ListView):
    model = Post

スニペットの HTML ファイルを作成し、CSS で調整し、スニペットをはります。

<ul class="list-group fw-bold">
    <span class="rad_5">
      <li class="list-group-item bg_fc_dark2">カテゴリ一覧</li>
      {% for category in category_list %}
        <li class="list-group-item bg_fc_dark">
          <a href='{% url "app:category" category.pk %}'>{{ category }}</a>
        </li>
      {% endfor %}
    </span>
</ul>
static/app/style.css
/* 自作 */
...
.bg_fc_dark2 {
  background-color: #181818;
  color: #ccc;
...
post_list.html
...
<!-- 768ピクセル以上 -->
    <div class="col-md-4">
      {% include 'snippets/category.html' %}
    </div>
...
post_detail.html
...
<!-- 768ピクセル以上 -->
    <div class="col-md-4">
      {% include 'snippets/category.html' %}
    </div>
...

管理用画面で扱えるように、モデルを登録します。

app/admin.py
...
from .models import Post, Category  # 追加
...
admin.site.register(Category)

マイグレート~動作確認

マイグレートして、サーバーを立ち上げます。

コマンド・プロンプト
python manage.py makemigrations
python manage.py migrate
python manage.py runserver 8080

http://localhost:8080/appにアクセスします。
カテゴリ表示はなく、記事には None と表示されています。

管理画面(http://localhost:8080/admin)にアクセスしてカテゴリを作成、記事で設定します。


再び、http://localhost:8080/appにアクセスします。
カテゴリ一覧にカテゴリが追加され、記事にもカテゴリが設定されました。
カテゴリのリンクをクリックすると、カテゴリの記事一覧が表示されます。(今回は記事が一つだけなので、表示は変わらず、URL のみの変化となります)


今回のプロジェクト

GitHub: https://github.com/Animalyzm/mikoto_project
今回のプロジェクトは、django/blog です。
(Git のコミット・メッセージは django_blog_2_bugfix です)
データベースなどは削除してるので、使用するにはマイグレートが必要になります。

コマンド・プロンプト
python manage.py makemigrations
python manage.py migrate

今回は、以上になります!
どうもありがとうございましたー♪

Discussion