Django超初心者シリーズ

10回 / 全10

【Django最終回】Markdownエディタとユーザー認証で本格的なブログシステムを完成させる

2025/08/10に公開

はじめに:プロ仕様のブログシステムを完成させよう

ついに最終回です!第1回で「Hello Django!」を表示してから、ここまで本当によく頑張りました。

振り返ってみると、僕たちは多くのことを学んできました:

  • 第1回:Djangoとは何か、基本概念
  • 第2回:環境構築とプロジェクト作成
  • 第3回:プロジェクト構造の理解
  • 第4回:最初のビューとテンプレート
  • 第5回:テンプレート継承と静的ファイル
  • 第6回:モデルとデータベース操作
  • 第7回:フォームとユーザー入力処理
  • 第8回:クラスベースビュー
  • 第9回:ModelFormでCRUD機能

そして今回、Markdownと認証の機能を追加してブログシステムを完成させます!

今回で完成する機能

  1. Markdownエディタ

    • リアルタイムプレビュー機能
    • シンタックスハイライト
    • 画像アップロード対応
  2. ユーザー認証とアクセス制限

    • ログイン・ログアウト機能
    • 記事の作成・編集・削除を管理者限定に
    • 記事に著者情報を追加
  3. プロフェッショナルな機能

    • CSS設計の最適化(機能別ファイル分割)
    • セキュアな記事管理
    • 著者情報の表示

それでは、Django学習の集大成を始めてみましょう!

Note

初めてこの記事を読む方へ:

この記事は連載の第10回目(最終回)です。ModelFormとCRUD機能の実装がまだの方は、第9回の記事をご覧ください。完成間近のコードはGitHubリポジトリで確認できます。

この記事で学べること

  • django-markdownxの導入と設定
  • リアルタイムプレビュー付きMarkdownエディタ
  • Markdownコンテンツのレンダリング
  • Django標準の認証システムの使い方
  • LoginView、LogoutViewの実装
  • LoginRequiredMixinでのアクセス制限
  • CSS設計のベストプラクティス
  • 保守性の高いコード構成
  • プロダクション品質のブログ構築

Markdownエディタの実装

なぜMarkdownが必要なのか?

ブログを書く上で、以下のような機能があると便利です:

  • 見出し強調などの文字装飾
  • コードブロックのシンタックスハイライト
  • リストや引用の表現
  • 画像の埋め込み

現在のブログでは、プレーンテキストしか入力できません。HTMLを直接書くのは大変ですし、セキュリティリスクもあります。

Markdownとは?

Markdown(マークダウン) は、プレーンテキストで文書を装飾できる軽量マークアップ言語です。HTMLを直接書くことなく、シンプルな記法で見出し、太字、リスト、リンクなどの装飾を表現できます。

初心者の方向けに、日常的によく使うMarkdown記法をご紹介します:

# 大見出し(h1タグ) ## 中見出し(h2タグ) ### 小見出し(h3タグ) **太字(strongタグ)***斜体(emタグ)* - 箇条書きリスト項目1 - 箇条書きリスト項目2 - ネストした子リスト 1. 番号付きリスト項目1 2. 番号付きリスト項目2 > 引用文(blockquoteタグ) `インラインコード(codeタグ)` ```python # コードブロック(preタグ + codeタグ) def hello_world(): print("Hello, Django!") ``` [リンクテキスト](https://www.djangoproject.com/) ![画像の代替テキスト](https://static.djangoproject.com/img/logos/django-logo-positive.png)

実際にMarkdownを試してみましょう!

上記のMarkdown記法をオンラインMarkdownエディタに入力すると、リアルタイムでHTMLに変換された結果を確認できます。

オンラインMarkdownエディタでのライブプレビュー例 - 左側にMarkdown記法、右側にレンダリング結果

Markdownを使うメリット:

  1. 学習コストが低い:HTMLよりもシンプルで覚えやすい
  2. 可読性が高い:マークアップなしでも内容が理解しやすい
  3. プレビュー機能:執筆しながらリアルタイムで結果を確認
  4. 広く採用されている:GitHub、Qiita、Zenn、Reddit、Discord等で利用
  5. セキュリティ:XSS攻撃のリスクがHTMLより低い
  6. プラットフォーム非依存:どのエディタでも編集可能
Tip

MarkdownとHTMLの関係

Markdownは最終的にHTMLに変換されます:

  • # 見出し<h1>見出し</h1>
  • **太字**<strong>太字</strong>
  • - リスト<ul><li>リスト</li></ul>

今回のブログシステムでは、django-markdownxを使ってこの変換を自動化します。

django-markdownxのインストール

まず、Markdownエディタを提供するパッケージをインストールします:

ターミナル
$ pip install django-markdownx $ pip install Pygments # コードハイライト用 $ pip install Pillow # 画像アップロード用

mysite/settings.pyのINSTALLED_APPSに追加:

mysite/settings.py
INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "markdownx", # 追加 "practice", "blog", ]

Markdownエディタの設定

mysite/settings.pyに以下を追加:

mysite/settings.py
# Markdownx設定 MARKDOWNX_EDITOR_RESIZABLE = True # エディタのサイズ変更を許可 MARKDOWNX_IMAGE_UPLOAD = True # 画像アップロード機能を有効化 MARKDOWNX_UPLOAD_URLS_PATH = "/markdownx/upload/" # アップロードURLパス MARKDOWNX_UPLOAD_CONTENT_TYPES = [ "image/jpeg", "image/png", "image/gif", ] # 許可する画像形式 # メディアファイルの設定(画像アップロード用) MEDIA_URL = "/media/" MEDIA_ROOT = BASE_DIR / "media" # プレビューのスタイル設定 MARKDOWNX_MARKDOWN_EXTENSIONS = [ "markdown.extensions.extra", # テーブル、脚注などの拡張機能 "markdown.extensions.codehilite", # コードのシンタックスハイライト "markdown.extensions.toc", # 目次生成機能 ] MARKDOWNX_MARKDOWN_EXTENSION_CONFIGS = { "markdown.extensions.codehilite": { "use_pygments": True, # Pygmentsを使用してハイライト "noclasses": True, # インラインスタイルを使う "pygments_style": "solarized-light", # 他のスタイルの名称は公式サイトを参照: https://pygments.org/styles/ } }
Tip

各設定の意味

エディタ設定:

  • MARKDOWNX_EDITOR_RESIZABLE: ユーザーがエディタのサイズを変更できるように
  • MARKDOWNX_IMAGE_UPLOAD: ドラッグ&ドロップで画像をアップロード可能に

Markdown拡張機能:

  • extra: テーブル、定義リスト、脚注などの追加記法
  • codehilite: コードブロックの言語別ハイライト表示
  • toc: [TOC]と書くと自動的に目次を生成

これらの設定により、より高機能なMarkdownエディタが使用できます。

URLパターンの設定

mysite/urls.pyにmarkdownxのURLを追加:

mysite/urls.py
from django.contrib import admin from django.urls import path, include urlpatterns = [ path("admin/", admin.site.urls), path("", include("blog.urls")), path("practice/", include("practice.urls")), path("markdownx/", include("markdownx.urls")), # 追加 ] # 開発環境でのメディアファイル配信(画像アップロード用) if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Note

なぜmarkdownx用のURLが必要?

markdownx/のURLパターンは以下の機能を提供します:

  • プレビュー機能: 入力したMarkdownをリアルタイムでHTMLに変換
  • 画像アップロード: エディタから画像をアップロードする際のエンドポイント

これらの機能は、django-markdownxがAjaxでバックエンドと通信することで実現されています。

メディアファイルの配信設定

開発環境では、Djangoがアップロードされた画像を配信できるように設定が必要です:

  • settings.DEBUGがTrueの時のみ有効(開発環境のみ)
  • 本番環境では、Webサーバー(NginxやApache)が画像を配信
  • これにより、Markdownエディタでアップロードした画像が即座に表示可能に

モデルの更新

blog/models.pyのPostモデルを更新して、MarkdownFieldを使用:

blog/models.py
from django.contrib.auth.models import User # 追加 from markdownx.models import MarkdownxField # 追加 class Post(models.Model): """ブログ記事のモデル""" title = models.CharField(max_length=200, verbose_name="タイトル") content = MarkdownxField(verbose_name="本文") # TextFieldから変更 is_featured = models.BooleanField(default=False, verbose_name="注目記事") is_published = models.BooleanField(default=False, verbose_name="公開状態") author = models.ForeignKey( User, on_delete=models.CASCADE, related_name="posts", verbose_name="著者", null=True, blank=True, ) # 追加 category = models.ForeignKey( Category, on_delete=models.SET_NULL, related_name="posts", verbose_name="カテゴリー", null=True, blank=True, ) tags = models.ManyToManyField( Tag, related_name="posts", verbose_name="タグ", blank=True ) created_at = models.DateTimeField(default=timezone.now, verbose_name="作成日時") updated_at = models.DateTimeField(auto_now=True, verbose_name="更新日時") class Meta: verbose_name = "記事" verbose_name_plural = "記事" ordering = ["-created_at"] # 新しい記事から順に並べる def __str__(self): return self.title
Note

なぜTextFieldからMarkdownxFieldに変更?

  • MarkdownxFieldは、内部的にはTextFieldを継承しています
  • 管理画面やフォームで自動的にMarkdownエディタが使用されます
  • データベースレベルでは通常のテキストとして保存されるため、既存データに影響なし

authorフィールドの追加について:

  • 誰が記事を書いたかを記録するため、最初から追加しています
  • null=Trueにより、既存の記事(著者なし)も問題なく扱えます
  • 後で認証機能を実装する際に、このフィールドを活用します

マイグレーションを実行:

ターミナル
$ python manage.py makemigrations Migrations for 'blog': blog\migrations\0005_post_author_alter_post_content.py + Add field author to post ~ Alter field content on post $ python manage.py migrate Operations to perform: Apply all migrations: admin, auth, blog, contenttypes, sessions Running migrations: Applying blog.0005_post_author_alter_post_content... OK

フォームの更新

blog/forms.pyのPostFormを更新:

blog/forms.py
from django import forms from markdownx.widgets import MarkdownxWidget # 追加 from .models import Category, Post class PostForm(forms.ModelForm): """記事作成・編集フォーム""" class Meta: model = Post fields = ["title", "content", "category", "tags", "is_featured", "is_published"] widgets = { "title": forms.TextInput( attrs={"class": "form-control", "placeholder": "記事のタイトルを入力"} ), "content": MarkdownxWidget( # 変更 attrs={ "class": "form-control", "placeholder": "Markdownで記事を書きます...\n\n# 見出し\n**強調**\n- リスト", } ), "category": forms.Select(attrs={"class": "form-control"}), "tags": forms.CheckboxSelectMultiple(attrs={"class": "form-check-input"}), "is_featured": forms.CheckboxInput(attrs={"class": "form-check-input"}), "is_published": forms.CheckboxInput(attrs={"class": "form-check-input"}), } labels = { "title": "タイトル", "content": "本文", "category": "カテゴリー", "tags": "タグ", "is_featured": "注目記事にする", "is_published": "公開する", } help_texts = { "title": "魅力的なタイトルを付けましょう", "tags": "複数選択できます", "is_published": "チェックを外すと下書きとして保存されます", "content": "Markdown記法が使えます: # 見出し、**太字**、*斜体*、[リンク](URL)、![画像](URL)", # 追加 }

Markdown専用CSSファイルの作成

これまでstyle.cssにすべてのスタイルを記述してきましたが、約900行と大きくなってきました。今回はMarkdown関連のスタイルを専用のCSSファイルとして分離し、管理しやすくしてみましょう。

blog/static/blog/css/markdown.cssを作成:

blog/static/blog/css/markdown.css
/* Markdownエディタ */ .form-page.markdown-editor { max-width: 1400px; } .markdownx { display: flex; gap: 1rem; margin-top: 0.5rem; margin-bottom: 2rem; } .markdownx-editor { flex: 1; min-height: 500px; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 14px; line-height: 1.6; padding: 1.5rem; border: 1px solid #ddd; border-radius: 4px; resize: vertical; } .markdownx-editor:focus { outline: none; border-color: #3498db; box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1); } .markdownx-preview { flex: 1; min-height: 500px; padding: 2rem; background: white; border: 1px solid #ddd; border-radius: 8px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); overflow-y: auto; } /* Markdown表示スタイル */ .markdownx-preview, .markdown-content, .post-body { /* 見出し */ h1, h2, h3, h4, h5, h6 { color: #2c3e50; margin-top: 2rem; margin-bottom: 1rem; line-height: 1.3; } h1:first-child, h2:first-child, h3:first-child { margin-top: 0; } h1 { font-size: 2rem; border-bottom: 2px solid #ecf0f1; padding-bottom: 0.5rem; } h2 { font-size: 1.75rem; } h3 { font-size: 1.5rem; } /* パラグラフ */ p { margin-bottom: 1.5rem; line-height: 1.8; color: #555; font-size: 1.1rem; } /* リンク */ a { color: #3498db; text-decoration: none; } a:hover { color: #2980b9; text-decoration: underline; } /* コードブロック */ pre { background: #f6f8fa; border: 1px solid #ddd; border-radius: 8px; padding: 1.5rem; margin: 1.5rem 0; overflow-x: auto; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.9rem; line-height: 1.5; } /* Pygments用のコンテナ */ .codehilite { background: #f6f8fa; border: 1px solid #ddd; border-radius: 8px; padding: 1.5rem; margin: 1.5rem 0; overflow-x: auto; } .codehilite pre { margin: 0; padding: 0; background: transparent; border: none; border-radius: 0; } /* インラインコード */ code { background: #f6f8fa; color: #e74c3c; padding: 0.2em 0.4em; border-radius: 3px; font-size: 0.9em; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; border: 1px solid #e1e8ed; } pre code { background: transparent; color: inherit; padding: 0; border: none; font-size: inherit; } /* 引用 */ blockquote { border-left: 4px solid #3498db; background: #f8f9fa; padding: 1.5rem; margin: 2rem 0; color: #7f8c8d; border-radius: 0 4px 4px 0; } blockquote p { margin-bottom: 1rem; } blockquote p:last-child { margin-bottom: 0; } /* リスト */ ul, ol { margin: 1.5rem 0; padding-left: 2rem; } li { margin: 0.5rem 0; line-height: 1.8; color: #555; } ul ul, ol ol, ul ol, ol ul { margin: 0.5rem 0; } /* テーブル */ table { width: 100%; border-collapse: collapse; margin: 2rem 0; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } th, td { padding: 0.75rem; text-align: left; border-bottom: 1px solid #ecf0f1; } th { background: #f8f9fa; color: #2c3e50; font-weight: 600; border-bottom: 2px solid #ddd; } tr:hover { background: rgba(52, 152, 219, 0.05); } /* 画像 */ img { max-width: 100%; height: auto; border-radius: 4px; margin: 1.5rem 0; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } /* 水平線 */ hr { border: none; height: 2px; background: #ecf0f1; margin: 2rem 0; } /* 強調 */ strong, b { font-weight: 600; color: #2c3e50; } em, i { font-style: italic; } } /* レスポンシブ */ @media (max-width: 1024px) { .markdownx { flex-direction: column; } .markdownx-editor, .markdownx-preview { width: 100%; min-height: 300px; } } @media (max-width: 768px) { .markdownx-preview, .markdown-content, .post-body { h1 { font-size: 1.75rem; } h2 { font-size: 1.5rem; } h3 { font-size: 1.25rem; } p { font-size: 1rem; } table { font-size: 0.9rem; } th, td { padding: 0.5rem; } } .markdownx-editor { font-size: 13px; padding: 1rem; } .markdownx-preview { padding: 1.5rem; } }
Tip

CSSファイルを機能別に分割する理由

今までstyle.css一つにすべてのスタイルを記述してきましたが、プロジェクトが成長するにつれて以下の問題が発生します:

  • ファイルが大きくなりすぎて、目的のスタイルを見つけるのが困難
  • 複数人で開発する際に、コンフリクトが発生しやすい
  • 特定の機能に関するスタイルだけを修正したい時に、影響範囲が分かりにくい

機能別にCSSファイルを分割することで:

  • 保守性の向上: Markdown関連のスタイルを修正したい時はmarkdown.cssだけを見れば良い
  • パフォーマンス向上: 必要なページでのみ必要なCSSを読み込める
  • 開発効率の向上: チーム開発でも作業分担しやすく、コンフリクトを減らせる

テンプレートの更新

blog/templates/blog/post_form.htmlを更新:

blog/templates/blog/post_form.html
{% extends 'blog/base.html' %} {% load static %} {% block title %}{{ page_title }} - My Blog{% endblock %} {% block extra_css %} {{ form.media.css }} <link rel="stylesheet" href="{% static 'blog/css/markdown.css' %}"> {% endblock %} {% block content %} <div class="form-page markdown-editor"> <div class="form-header"> <h2>{{ page_title }}</h2> <p class="form-description"> Markdownで記事を書きましょう。左側に入力すると、右側にプレビューが表示されます。 </p> </div> <div class="form-container"> <form method="post" class="post-form"> {% csrf_token %} <!-- エラーメッセージ --> {% if form.non_field_errors %} <div class="alert alert-danger"> {{ form.non_field_errors }} </div> {% endif %} <!-- タイトル --> <div class="form-group"> {{ form.title.label_tag }} {{ form.title }} {% if form.title.help_text %} <small class="form-help">{{ form.title.help_text }}</small> {% endif %} {% if form.title.errors %} <div class="error-message">{{ form.title.errors }}</div> {% endif %} </div> <!-- 本文(Markdownエディタ) --> <div class="form-group"> {{ form.content.label_tag }} {% if form.content.help_text %} <small class="form-help">{{ form.content.help_text }}</small> {% endif %} {{ form.content }} {% if form.content.errors %} <div class="error-message">{{ form.content.errors }}</div> {% endif %} </div> <!-- カテゴリー --> <div class="form-group"> {{ form.category.label_tag }} {{ form.category }} {% if form.category.errors %} <div class="error-message">{{ form.category.errors }}</div> {% endif %} </div> <!-- タグ --> <div class="form-group"> {{ form.tags.label_tag }} <div class="checkbox-group"> {{ form.tags }} </div> {% if form.tags.help_text %} <small class="form-help">{{ form.tags.help_text }}</small> {% endif %} {% if form.tags.errors %} <div class="error-message">{{ form.tags.errors }}</div> {% endif %} </div> <!-- オプション --> <div class="form-group"> <div class="form-check"> {{ form.is_featured }} {{ form.is_featured.label_tag }} </div> <div class="form-check"> {{ form.is_published }} {{ form.is_published.label_tag }} {% if form.is_published.help_text %} <small class="form-help">{{ form.is_published.help_text }}</small> {% endif %} </div> </div> <!-- ボタン --> <div class="form-actions"> <button type="submit" class="btn btn-primary"> {{ button_text }} </button> <a href="{% url 'post_list' %}" class="btn btn-secondary"> キャンセル </a> </div> </form> </div> </div> {% endblock %} {% block extra_js %} {{ form.media.js }} {% endblock %}
Note

form.mediaの役割

{{ form.media.css }}{{ form.media.js }}は、django-markdownxが必要とする:

  • CSS: エディタの基本スタイル
  • JavaScript: プレビュー機能、画像アップロード機能などの動作

これらを読み込むことで、Markdownエディタが正しく動作します。

Markdownコンテンツの表示

記事詳細ページでMarkdownをHTMLに変換して表示する必要があります。

blog/templatetags/markdown_extras.pyを作成:

blog/templatetags/markdown_extras.py
from django import template from django.utils.safestring import mark_safe import markdown register = template.Library() @register.filter(name="markdown_to_html") def markdown_to_html(text): """MarkdownテキストをHTMLに変換""" # Markdown拡張機能の設定 md = markdown.Markdown( # 利用できる拡張機能の一覧は以下の公式ドキュメントから確認することができます # https://python-markdown.github.io/extensions/ extensions=[ "extra", # テーブル、脚注、定義リストなど "codehilite", # コードブロックのシンタックスハイライト "toc", # 目次生成([TOC]タグ) "tables", # テーブル記法 "fenced_code", # ```で囲むコードブロック ], extension_configs={ "codehilite": { "noclasses": True, "pygments_style": "solarized-light", } }, ) html = md.convert(text) return mark_safe(html)
Tip

Markdown拡張機能の説明

  • extra: Markdown標準にない便利な記法を追加
    • テーブル: | 列1 | 列2 |
    • 脚注: [^1]
    • 定義リスト: 用語 : 説明
  • codehilite: コードブロックに言語を指定してハイライト
    ```python def hello(): print("Hello") ```
  • toc: [TOC]と書いた場所に自動的に目次を生成
  • tables: より高度なテーブル記法をサポート
  • fenced_code: バッククォート3つでコードブロックを作成

mark_safe()の重要性: DjangoはセキュリティのためHTMLを自動エスケープしますが、 MarkdownからHTMLへの変換結果は安全なので、mark_safe()で エスケープを無効化しています。

blog/templates/blog/post_detail.htmlを更新:

blog/templates/blog/post_detail.html
{% extends 'blog/base.html' %} {% load static %} {% load markdown_extras %} <!-- カスタムテンプレートタグを読み込み --> {% block title %}{{ post.title }} - My Blog{% endblock %} {% block extra_css %} <!-- Markdownコンテンツ用のCSS --> <link rel="stylesheet" href="{% static 'blog/css/markdown.css' %}"> {% endblock %} {% block content %} {% if post %} <article class="post-detail"> <!-- 既存のタイトルとメタ情報 --> <h2>{{ post.title }}</h2> {% if post.category %} <p class="post-category">カテゴリー: {{ post.category.name }}</p> {% endif %} <p class="post-meta"> 投稿日: {{ post.created_at }} {% if post.author %} | 著者: {{ post.author.username }} {% endif %} </p> <!-- ここを変更:Markdownレンダリング --> <div class="post-body markdown-content"> {{ post.content|markdown_to_html }} <!-- linebreaksからmarkdown_to_htmlに変更 --> </div> <!-- 以下、タグ表示、編集ボタン、コメントセクションなどは既存のまま --> <!-- ... --> </article> {% endif %} {% endblock %}

主な変更点:

  • {% load markdown_extras %}を追加してカスタムフィルタを読み込み
  • extra_cssブロックでMarkdown用のCSSを読み込み
  • post-bodymarkdown-contentクラスを追加
  • {{ post.content|linebreaks }}{{ post.content|markdown_to_html }}に変更
  • 著者情報の表示を追加

記事一覧ページの修正

同様に、記事一覧でもMarkdownコンテンツが適切に表示されるように修正します。

blog/templates/blog/includes/post_card.htmlを修正:

blog/templates/blog/includes/post_card.html
{% load markdown_extras %} <!-- カスタムテンプレートタグを追加 --> <article class="post-card"> <div class="post-card-header"> {% if post.category %} <span class="category">{{ post.category.name }}</span> {% else %} <span class="category">未分類</span> {% endif %} <span class="date">{{ post.created_at }}</span> </div> <h3 class="post-card-title"> <a href="{% url 'post_detail' post_id=post.id %}"> {{ post.title }} </a> </h3> <p class="post-card-excerpt"> <!-- MarkdownをHTMLに変換してからテキストのみを抽出 --> {{ post.content|markdown_to_html|striptags|truncatechars:120 }} </p> <!-- 既存のタグ表示、アクションボタン部分はそのまま --> <!-- ... --> </article>

主な変更点:

  • {% load markdown_extras %}でカスタムフィルタを読み込み
  • {{ post.content|markdown_to_html|striptags|truncatechars:120 }}で以下を実現:
    1. MarkdownをHTMLに変換
    2. HTMLタグを除去(プレーンテキストに)
    3. 120文字で省略
Tip

フィルターの連鎖について

Djangoテンプレートでは、フィルターを|で連鎖できます:

  • markdown_to_html: Markdown→HTML変換
  • striptags: HTMLタグをすべて除去
  • truncatechars:120: 120文字で省略

この順序が重要で、先にHTMLに変換してからタグを除去することで、 見出しや強調などの装飾を適切に処理できます。

Note

Pygmentsによるコードハイライトの仕組み

コードハイライトには主に2つの実装方法があります:

1. CSSクラス方式(デフォルト)

  • コードにクラス名のみを付与し、スタイルは別途CSSファイルで定義
  • 複数ページで同じCSSを共有できるため効率的
  • カスタマイズしやすい

2. インラインスタイル方式(今回採用)

  • noclasses=Trueの設定により、各要素に直接スタイルを埋め込む
  • 追加のCSSファイル読み込みが不要
  • すぐに使える手軽さがメリット

今回はnoclasses=Trueを設定しているため、pygments_styleに設定したテーマ(solarized-light)の配色をHTMLに直接埋め込みます。これにより、追加のスタイルシートなしで美しいシンタックスハイライトが実現できます。

管理画面でもMarkdownエディタを使用

blog/admin.pyを更新:

blog/admin.py
from django.contrib import admin from markdownx.admin import MarkdownxModelAdmin # 追加 from .models import Category, Tag, Post, Comment # CategoryAdminとTagAdminは既存のまま @admin.register(Post) class PostAdmin(MarkdownxModelAdmin): # admin.ModelAdminから変更 """記事の管理画面""" list_display = ["title", "category", "author", "is_published", "is_featured", "created_at"] list_filter = ["is_published", "is_featured", "category", "author", "created_at"] search_fields = ["title", "content", "author__username"] ordering = ["-created_at"] date_hierarchy = "created_at" # フィールドをグループ化して表示 fieldsets = [ ("基本情報", {"fields": ["title", "content", "author"]}), ("分類", {"fields": ["category", "tags"]}), ("オプション", {"fields": ["is_published", "is_featured"]}), ] def save_model(self, request, obj, form, change): """保存時の処理をカスタマイズ""" if not change: # 新規作成時 obj.author = request.user # 著者を現在のユーザーに自動設定 super().save_model(request, obj, form, change)
Note

MarkdownxModelAdminの利点

MarkdownxModelAdminadmin.ModelAdminを継承し、Markdown関連の機能を追加したクラスです:

  • 自動的にMarkdownエディタが適用: MarkdownxFieldを持つモデルに対して、管理画面でも自動的にMarkdownエディタが使用される
  • プレビュー機能: 管理画面でもリアルタイムプレビューが利用可能
  • 画像アップロード: ドラッグ&ドロップでの画像アップロード機能も動作

通常のadmin.ModelAdminからMarkdownxModelAdminに変更するだけで、これらの機能が有効になります。

動作確認

ここまでの設定が完了したら、実際にMarkdownエディタを試してみましょう。

  1. 開発サーバーを起動

    ターミナル
    $ python manage.py runserver
  2. ヘッダーの「新規投稿」をクリックし、新規記事作成ページを表示

  3. Markdownエディタの確認

左側の入力エリアにMarkdownを入力すると、右側にリアルタイムでプレビューが表示されます。

試してみましょう:

# Djangoでブログを作ってみた! ## はじめに Djangoの**ModelForm****クラスベースビュー**を使って、 本格的なブログシステムを構築しました。 ### 実装した機能 - CRUD機能 - Markdownエディタ - コメント機能 - カテゴリー・タグ ## コードサンプル ```python from django.views.generic import CreateView from .models import Post class PostCreateView(CreateView): model = Post fields = ['title', 'content'] template_name = 'blog/post_form.html' ``` ## まとめ Djangoを使えば、短期間で高機能なWebアプリケーションが作れます!
  1. 記事を保存して表示確認

保存した記事を表示すると、Markdownが美しくHTMLに変換されて表示されます:

Markdown記事の表示結果

  • 見出しが階層的に表示される(h1、h2、h3)
  • 太字や斜体などの文字装飾が適用される
  • リスト項目が整形されて表示される
  • コードブロックがシンタックスハイライト付きで表示される
Tip

画像のアップロード

Markdownエディタには画像アップロード機能も搭載されています:

  1. エディタに画像をドラッグ&ドロップ
  2. 自動的にアップロードされ、Markdown記法が挿入される
  3. プレビューで即座に確認可能

アップロードされた画像はmedia/markdownx/に保存されます。

管理画面での動作確認

管理画面でもMarkdownエディタが使えるようになっているか確認してみましょう。

  1. 管理画面にアクセス

    http://localhost:8000/admin/
  2. 記事の追加または編集画面を開く

    • 「記事」→「記事を追加」または既存の記事をクリック
  3. Markdownエディタの確認

管理画面のMarkdownエディタ

管理画面でも同様の機能が使えます:

  • デフォルトでは上下レイアウト(上部にエディタ、下部にプレビュー)
  • 画像のドラッグ&ドロップアップロード機能
  • リアルタイムプレビュー
Note

レイアウトの違いについて

管理画面ではdjango-markdownxのデフォルトスタイルが適用されるため上下レイアウトですが、 記事作成・編集ページでは独自のCSSを適用しているため左右レイアウトになっています。 管理画面でも左右レイアウトにしたい場合は、管理画面用のCSSをカスタマイズすることで変更可能です。

すべてが正常に動作していることを確認できます。

requirements.txtの作成

ここまでで複数のPythonパッケージをインストールしてきました。プロジェクトの依存関係を管理するために、requirements.txtファイルを作成してみましょう。

requirements.txtとは?

requirements.txtは、Pythonプロジェクトで使用するパッケージとそのバージョンを記録するファイルです。このファイルがあることで:

  • 環境の再現性:他の開発者や本番環境で同じパッケージ構成を再現できる
  • バージョン管理:特定のバージョンで動作することを保証
  • チーム開発:メンバー間で統一された開発環境を維持
  • デプロイ時の安全性:本番環境で必要なパッケージを確実にインストール

requirements.txtの作成

現在インストールされているパッケージ一覧を出力します:

ターミナル
$ pip freeze > requirements.txt

作成されたrequirements.txtの内容を確認:

コマンド プロンプト
$ type requirements.txt

以下のような内容が出力されます:

requirements.txt
asgiref==3.9.1 Django==5.2.4 django-markdownx==4.0.9 Markdown==3.8.2 pillow==11.3.0 Pygments==2.19.2 sqlparse==0.5.3 tzdata==2025.2

requirements.txtからのインストール

他の環境でこのプロジェクトをセットアップする場合:

コマンド プロンプト
# 仮想環境を作成・有効化 $ python -m venv venv $ venv\Scripts\activate # requirements.txtからパッケージをインストール $ pip install -r requirements.txt
Tip

requirements.txtのベストプラクティス

  • 定期的な更新:新しいパッケージをインストールしたらpip freeze > requirements.txtで更新
  • バージョン固定==でバージョンを固定することで環境の一貫性を保つ

ユーザー認証の実装

Django標準の認証システム

Djangoには強力な認証システムが標準で含まれています。これを使えば、安全なログイン機能を簡単に実装できます。

まず、認証用のURLパターンを追加してみましょう。

mysite/urls.pyを修正:

mysite/urls.py
from django.contrib import admin from django.contrib.auth import views as auth_views # 追加 from django.urls import path, include from django.conf import settings from django.conf.urls.static import static urlpatterns = [ path("admin/", admin.site.urls), # 認証関連のURL(blogの前に配置) path("login/", auth_views.LoginView.as_view(template_name="blog/login.html"), name="login"), path("logout/", auth_views.LogoutView.as_view(), name="logout"), path("", include("blog.urls")), path("practice/", include("practice.urls")), path("markdownx/", include("markdownx.urls")), ]

ログインテンプレートの作成

blog/templates/blog/login.htmlを作成:

blog/templates/blog/login.html
{% extends 'blog/base.html' %} {% block title %}ログイン - My Blog{% endblock %} {% block content %} <div class="auth-page"> <div class="auth-container"> <h2>ログイン</h2> <form method="post" class="auth-form"> {% csrf_token %} {% if form.non_field_errors %} <div class="alert alert-danger"> {{ form.non_field_errors }} </div> {% endif %} <div class="form-group"> {{ form.username.label_tag }} <input type="text" name="username" class="form-control" required value="{{ form.username.value|default:'' }}"> {% if form.username.errors %} <div class="error-message">{{ form.username.errors }}</div> {% endif %} </div> <div class="form-group"> {{ form.password.label_tag }} <input type="password" name="password" class="form-control" required> {% if form.password.errors %} <div class="error-message">{{ form.password.errors }}</div> {% endif %} </div> <button type="submit" class="btn btn-primary btn-block"> ログイン </button> </form> </div> </div> {% endblock %}

認証設定の追加

mysite/settings.pyに追加:

mysite/settings.py
# ログイン・ログアウト後のリダイレクト先 LOGIN_URL = 'login' LOGIN_REDIRECT_URL = 'post_list' LOGOUT_REDIRECT_URL = 'post_list'

ヘッダーに認証リンクを追加

blog/templates/blog/base.htmlのヘッダー部分を修正:

blog/templates/blog/base.html
<header> <nav> <h1> <a href="{% url 'post_list' %}"> <img src="{% static 'blog/images/logo.png' %}" alt="My Blog" height="40"> <span>My Blog</span> </a> </h1> <ul> <li><a href="{% url 'post_list' %}">ホーム</a></li> <li><a href="{% url 'post_search' %}">検索</a></li> {% if user.is_authenticated %} <li><a href="{% url 'post_create' %}">新規投稿</a></li> <li><a href="{% url 'about' %}">About</a></li> <li> <form method="post" action="{% url 'logout' %}" style="display: inline;"> {% csrf_token %} <button type="submit" class="logout-btn">ログアウト</button> </form> </li> {% else %} <li><a href="{% url 'about' %}">About</a></li> <li><a href="{% url 'login' %}">ログイン</a></li> {% endif %} </ul> </nav> </header>

アクセス制限の実装

記事に著者情報の自動設定

先ほどPostモデルに追加したauthorフィールドを、記事作成時に自動的に設定するようにします。

blog/views.pyのPostCreateViewを修正:

blog/views.py
from django.contrib.auth.mixins import LoginRequiredMixin # 追加 class PostCreateView(LoginRequiredMixin, CreateView): """記事作成ビュー(ログイン必須)""" model = Post form_class = PostForm template_name = 'blog/post_form.html' success_url = reverse_lazy('post_list') def form_valid(self, form): """フォームのバリデーション成功時""" # 著者を現在のユーザーに設定 form.instance.author = self.request.user messages.success(self.request, '記事を作成しました!') return super().form_valid(form) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["page_title"] = "新規記事作成" context["button_text"] = "作成" return context

自分の記事のみ編集可能にする

blog/views.pyを修正:

blog/views.py
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin # UserPassesTestMixinを追加 class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView): """記事編集ビュー(著者またはスーパーユーザーのみ)""" model = Post form_class = PostForm template_name = "blog/post_form.html" def test_func(self): """編集権限のチェック""" post = self.get_object() # 著者本人またはスーパーユーザーのみ編集可能 return post.author == self.request.user or self.request.user.is_superuser def get_success_url(self): """更新成功時のリダイレクト先""" return reverse_lazy("post_detail", kwargs={"post_id": self.object.pk}) def form_valid(self, form): messages.success(self.request, "記事を更新しました!") return super().form_valid(form) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["page_title"] = f"記事編集: {self.object.title}" context["button_text"] = "更新" return context class PostDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView): """記事削除ビュー(著者またはスーパーユーザーのみ)""" model = Post template_name = 'blog/post_confirm_delete.html' def test_func(self): """削除権限のチェック""" post = self.get_object() return post.author == self.request.user or self.request.user.is_superuser def get_success_url(self): """削除成功時のリダイレクト先""" messages.success(self.request, "記事を削除しました。") return reverse_lazy("post_list")
Tip

Mixinの順序に注意!

クラスを継承する際の順序は重要です:

class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView): # 正しい class PostUpdateView(UpdateView, LoginRequiredMixin, UserPassesTestMixin): # 間違い

Mixinは必ず最初に記述します。Pythonは左から右へ順番にメソッドを探すためです。

編集・削除リンクの表示制御

編集・削除リンクは複数の場所で使用されているため、以下のテンプレートを修正します。

1. 記事カード(一覧ページ)

blog/templates/blog/includes/post_card.htmlを修正:

blog/templates/blog/includes/post_card.html
<!-- 既存のコンテンツ --> <div class="post-actions"> <a href="{% url 'post_detail' post_id=post.pk %}" class="read-more"> 続きを読む → </a> {% if user.is_authenticated %} {% if post.author == user or user.is_superuser %} <div class="admin-actions"> <a href="{% url 'post_edit' pk=post.pk %}" class="btn-link btn-edit"> 編集 </a> <a href="{% url 'post_delete' pk=post.pk %}" class="btn-link btn-delete"> 削除 </a> </div> {% endif %} {% endif %} </div>

2. 記事詳細ページ

blog/templates/blog/post_detail.htmlの記事下部にも編集ボタンがあるため、同様に修正:

blog/templates/blog/post_detail.html
<p><a href="{% url 'post_list' %}">← 記事一覧に戻る</a></p> <!-- 編集・削除ボタン(アクセス制限付き) --> {% if user.is_authenticated %} {% if post.author == user or user.is_superuser %} <div class="post-actions"> <a href="{% url 'post_edit' pk=post.pk %}" class="btn btn-primary"> 編集 </a> </div> {% endif %} {% endif %}

認証関連のスタイル追加

認証用CSSファイルの作成

認証関連のスタイルも専用のCSSファイルとして分離します。

blog/static/blog/css/auth.cssを作成:

blog/static/blog/css/auth.css
/* 認証ページ */ .auth-page { min-height: calc(100vh - 200px); display: flex; align-items: center; justify-content: center; padding: 2rem; } .auth-container { background: white; padding: 3rem; border-radius: 8px; box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1); width: 100%; max-width: 400px; } .auth-container h2 { text-align: center; color: #2c3e50; margin-bottom: 2rem; } .auth-form .form-group { margin-bottom: 1.5rem; } .auth-form label { display: block; margin-bottom: 0.5rem; color: #333; font-weight: 500; } .btn-block { width: 100%; }

blog/static/blog/css/style.cssに追加:

blog/static/blog/css/style.css
.logout-btn { background: none; border: none; color: #ecf0f1; font: inherit; cursor: pointer; text-decoration: none; padding: 0; margin: 0; font-size: inherit; } .logout-btn:hover { opacity: 0.8; }

ログインテンプレートの修正

blog/templates/blog/login.htmlを修正して、CSSファイルを読み込むように:

blog/templates/blog/login.html
{% extends 'blog/base.html' %} {% load static %} {% block title %}ログイン - My Blog{% endblock %} {% block extra_css %} <link rel="stylesheet" href="{% static 'blog/css/auth.css' %}"> {% endblock %} <!-- 既存のコンテンツはそのまま -->
Note

CSSファイルの整理

プロジェクトが成長するにつれて、CSSファイルを機能別に分割することで、より保守しやすくなります:

  • style.css: 基本的なレイアウトやコンポーネント(既存の約900行)
  • markdown.css: Markdown関連のスタイル(新規作成)
  • auth.css: 認証ページのスタイル(新規作成)

将来的には以下のような分割も検討できます:

  • forms.css: フォーム共通のスタイル
  • components.css: ボタンやカードなどの再利用可能なコンポーネント
  • responsive.css: レスポンシブ対応

この方法により、各ページで必要なCSSのみを読み込むことができ、パフォーマンスも向上します。

動作確認と成果物の展示

完成したブログシステムの機能一覧

ついに、僕たちのブログシステムが完成しました!10回の連載で実装した機能を振り返ってみましょう:

基本機能

  • 記事の作成・編集・削除:ModelFormとクラスベースビューによる完全なCRUD機能
  • 記事一覧表示:ページネーション付きの見やすい一覧
  • 記事詳細表示:個別記事の詳細ページ
  • カテゴリー・タグ管理:記事の分類・検索機能

高度な機能

  • Markdownエディタ:リアルタイムプレビュー付きの記事作成
  • シンタックスハイライト:コードブロックの美しい表示
  • 画像アップロード:ドラッグ&ドロップでの画像投稿
  • ユーザー認証:ログイン・ログアウト機能
  • アクセス制限:著者のみが編集・削除可能

デザイン・UI/UX

  • レスポンシブデザイン:モバイル・タブレット対応
  • モダンなUI:カード型レイアウトとホバーエフェクト
  • 直感的なナビゲーション:分かりやすいメニュー構成
  • 読みやすいタイポグラフィ:記事の可読性を重視

最終動作確認

完成したシステムを実際に使ってみましょう。

1. 管理者アカウントの作成(まだの場合)

ターミナル
$ python manage.py createsuperuser Username: admin Email address: admin@example.com Password: Password (again): Superuser created successfully.

2. 開発サーバーの起動

ターミナル
$ python manage.py runserver

3. ブログシステムの操作確認

ログイン機能のテスト:

  1. ブラウザで http://localhost:8000 にアクセス
  2. 右上の「ログイン」をクリック
  3. 作成した管理者アカウントでログイン
  4. ログイン後、「ログアウト」ボタンが表示されることを確認

Markdownエディタのテスト:

  1. 「新規投稿」をクリック

  2. 以下の内容を入力して、リアルタイムプレビューを確認:

    • タイトル: Django学習の成果発表!

    • カテゴリー: Django入門

    • タグ: Djangoチュートリアル を選択

    • 公開する: 選択する

    • 本文:

      ## 完成したブログシステム **10回の連載**を通じて、以下の技術を習得しました: ### 学んだ技術 - Django MVTパターン - モデル設計とマイグレーション - クラスベースビュー - ModelForm - ユーザー認証 - Markdownエディタ ### サンプルコード ```python from django.views.generic import CreateView from django.contrib.auth.mixins import LoginRequiredMixin class PostCreateView(LoginRequiredMixin, CreateView): model = Post form_class = PostForm template_name = 'blog/post_form.html' ``` > **感想:** Djangoの学習を通じて、Webアプリケーション開発の > 全体像が理解できました! ## 次のステップ - Django REST Frameworkの学習 - デプロイの実践 - テスト駆動開発 ![Django Logo](https://static.djangoproject.com/img/logos/django-logo-positive.png)
  3. 右側のプレビューでMarkdownが正しくHTMLに変換されることを確認

  4. 記事を保存して詳細ページを表示

  5. シンタックスハイライトが適用されていることを確認

権限管理のテスト:

  1. 記事詳細ページで「編集」ボタンが表示されることを確認
  2. ログアウトして同じ記事を表示
  3. 編集ボタンが非表示になることを確認

成果物のスクリーンショット

ホームページ(記事一覧) ブログホームページ モダンなカード型レイアウトで記事一覧を表示

Markdownエディタ Markdownエディタ画面 左側にMarkdown入力、右側にリアルタイムプレビュー

記事詳細ページ 記事詳細ページ1 記事詳細ページ2

美しくレンダリングされたMarkdown記事

認証システム ログイン画面 シンプルで使いやすいログイン画面

技術的な達成内容

アーキテクチャ面

  • MVTパターンの理解:Model、View、Templateの分離
  • 関心の分離:各機能が独立したモジュールとして実装
  • 再利用可能なコンポーネント:テンプレート継承とinclude

データベース設計

  • 正規化されたモデル設計:Post、Category、Tag、Userの適切な関係
  • 外部キーとManyToManyの活用:データの整合性を保持
  • マイグレーションの理解:データベース変更の安全な管理

セキュリティ

  • CSRF保護:フォーム送信時の偽造リクエスト防止
  • ユーザー認証:Django標準の認証システム活用
  • アクセス制限:LoginRequiredMixinとUserPassesTestMixin
  • XSS対策:テンプレートの自動エスケープ

フロントエンド

  • レスポンシブデザイン:CSS Grid、Flexboxの活用
  • モダンなUI/UX:ホバーエフェクト、アニメーション
  • アクセシビリティ:セマンティックHTML、適切なコントラスト

コード品質

  • DRY原則:重複コードの排除
  • 保守性:機能別のファイル分割(CSS、テンプレート)
  • 拡張性:新機能追加が容易な設計

まとめとこれから

10回で学んだことの総まとめ

ついに完走しました!振り返ってみると、本当に多くのことを学びましたね:

  1. Django基礎

    • MVTパターンの理解
    • プロジェクト構造
    • 開発環境の構築
  2. ビューとテンプレート

    • 関数ベースビュー
    • クラスベースビュー
    • テンプレート継承
  3. データベース操作

    • モデルの定義
    • マイグレーション
    • QuerySetの使い方
  4. フォーム処理

    • forms.Form
    • ModelForm
    • バリデーション
  5. 認証とセキュリティ

    • ユーザー認証
    • アクセス制限
    • CSRF対策

Djangoエンジニアとしての次のステップ

このシリーズでDjangoの基本をマスターしたあなたには、さらなる成長の道が待っています:

中級レベルへのステップ

  1. Django REST Framework:API開発でフロントエンドと分離
  2. 非同期処理:Celeryでバックグラウンドタスク
  3. リアルタイム機能:Django ChannelsでWebSocket通信
  4. テスト駆動開発:TDDで品質の高いコードを作成
  5. デプロイとインフラ:Docker、AWS/GCPの活用

高級レベルへの道

  • パフォーマンス最適化:キャッシュ、データベースチューニング
  • スケーラビリティ:ロードバランサー、マイクロサービス
  • セキュリティ強化:OAuth、セキュリティヘッダー

おすすめの学習リソース

公式ドキュメントと書籍

オンラインリソース

シリーズを終えての感想

10回の連載、本当にお疲れ様でした!

最初は「Hello Django!」から始まって、今では本格的なブログシステムを作ることができました。これは素晴らしい成果です。

あなたが達成したこと

  • ゼロからのWebアプリ構築:最初の一歩から完成まで
  • 現代的な技術スタック:Python + Django + HTML/CSS/JS
  • 実用的な機能:実際に使えるブログシステム
  • プログラミングの思考法:問題を分解して解決する力

ぼくがこのシリーズを作成した想い

このシリーズを作成した理由は、「Djangoを学びたいけれど、何から始めていいか分からない」という方のためでした。

プログラミングの学習は、理論だけではなく実際に作ってみることが一番大切です。だからこそ、シンプルな「Hello World」から始めて、最終的には本格的なアプリケーションを完成させる、というストーリー仕立てにしました。

あなたへのメッセージ

プログラミングの道に終わりはありません。でも、それが楽しいところでもあります。常に新しいことを学び、より良いコードを書けるようになる。その過程を楽しんでください。

失敗を恐れないでください。 エラーは学習の機会です。バグは理解を深めるチャンスです。一つ一つのエラーを乗り越えるたびに、あなたは成長しています。

作ることを続けてください。 このブログシステムをベースに、新しい機能を追加してみてください。コメント機能、いいね機能、タグ検索、RSSフィード…。アイデアは無限です。

あなたが作ったブログが、誰かの役に立つことを願っています。そして、このチュートリアルがあなたのDjango journeyの良いスタートになったなら、これ以上の喜びはありません。

Keep coding, keep learning, and keep building amazing things with Django! 🚀

完成したコード

最終的なコードは以下で確認できます:

https://github.com/techarm/django-blog-tutorial/tree/10-markdown-auth-complete

実装でつまずいたときは、このリポジトリと比較してみてください。各章ごとにブランチが分かれているので、段階的に確認することもできます。

プロジェクト構成の最終形

django-blog/ ├── mysite/ │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── blog/ │ ├── migrations/ │ ├── static/blog/ │ │ ├── css/ │ │ │ ├── style.css │ │ │ ├── markdown.css │ │ │ └── auth.css │ │ ├── images/ │ │ └── js/ │ ├── templates/blog/ │ │ ├── includes/ │ │ └── [各種テンプレート] │ ├── templatetags/ │ │ ├── __init__.py │ │ ├── blog_extras.py │ │ └── markdown_extras.py │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── models.py │ ├── mixins.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── media/ ├── manage.py └── requirements.txt

Django超初心者向けシリーズ 完結

Thank you for learning Django with me! 🎉

"

次回予告

このDjangoシリーズが好評でしたら、続編として以下のような内容を検討しています:

  • Django REST FrameworkでAPI開発
  • React + Djangoでモダンなフルスタック開発
  • Dockerを使った開発環境構築
  • AWS/GCPを使った本格デプロイ

ご意見・ご感想をお聞かせください!

この記事はいかがでしたか?

もしこの記事が参考になりましたら、
高評価をいただけると大変嬉しいです!

--

皆様からの応援が励みになります。ありがとうございます! ✨

Django超初心者シリーズについて

このシリーズでは、プログラミング初心者の方でもDjangoを使ったWebアプリケーション開発ができるようになることを目標としています。わからないことがあれば、コメント欄でお気軽にご質問ください!