当前位置: 首页 > news >正文

使用Django从零开始构建一个个人博客系统

目录

  • 使用Django从零开始构建一个个人博客系统
    • 1. 项目概述与技术栈选择
      • 1.1 为什么选择Django构建博客?
      • 1.2 博客系统功能规划
      • 1.3 技术栈确定
    • 2. 开发环境搭建与项目初始化
      • 2.1 环境准备与依赖安装
      • 2.2 创建Django项目和应用程序
      • 2.3 配置基础设置
    • 3. 数据模型设计
      • 3.1 核心模型关系分析
      • 3.2 实现数据模型
      • 3.3 创建数据库迁移并应用
    • 4. 管理后台配置
      • 4.1 配置Django管理后台
    • 5. 视图和URL配置
      • 5.1 博客视图实现
      • 5.2 用户认证视图
      • 5.3 URL路由配置
    • 6. 表单和模板系统
      • 6.1 评论表单实现
      • 6.2 基础模板设计
      • 6.3 文章列表模板
      • 6.4 文章详情模板
      • 6.5 侧边栏模板
    • 7. 完整项目部署配置
      • 7.1 生产环境设置
      • 7.2 Gunicorn配置
      • 7.3 部署脚本
      • 7.4 Nginx配置
    • 8. 代码自查与优化
      • 8.1 安全自查
      • 8.2 性能优化
      • 8.3 完整的测试套件
    • 9. 项目总结与扩展建议
      • 9.1 项目成果
      • 9.2 扩展功能建议
      • 9.3 进一步学习路径

『宝藏代码胶囊开张啦!』—— 我的 CodeCapsule 来咯!✨
写代码不再头疼!我的新站点 CodeCapsule 主打一个 “白菜价”+“量身定制”!无论是卡脖子的毕设/课设/文献复现,需要灵光一现的算法改进,还是想给项目加个“外挂”,这里都有便宜又好用的代码方案等你发现!低成本,高适配,助你轻松通关!速来围观 👉 CodeCapsule官网

使用Django从零开始构建一个个人博客系统

在数字化时代,拥有个人博客不仅是技术展示的窗口,更是思想传播的平台。本文将手把手带你用Django构建功能完整的博客系统,从环境搭建到部署上线,掌握全栈开发核心技能。

1. 项目概述与技术栈选择

1.1 为什么选择Django构建博客?

Django作为Python最流行的Web框架,以其"自带电池"的理念为开发者提供了完整的工具链。对于博客系统而言,Django具备以下优势:

  • 内置管理后台:无需编写代码即可获得功能强大的内容管理系统
  • ORM支持:优雅的数据库操作,支持多种数据库后端
  • 模板系统:清晰的MVC分离,便于前后端协作
  • 安全机制:内置CSRF保护、SQL注入防护等安全特性
  • 丰富的生态:海量第三方包,快速实现各种功能需求

1.2 博客系统功能规划

个人博客系统
内容管理
用户认证
前台展示
文章CRUD
分类管理
标签管理
评论系统
用户注册
登录登出
权限控制
文章列表
文章详情
分类归档
搜索功能

1.3 技术栈确定

  • 后端框架:Django 4.2 LTS
  • 数据库:SQLite(开发) / PostgreSQL(生产)
  • 前端:Bootstrap 5 + Django模板
  • 部署:Gunicorn + Nginx
  • 其他:Django Crispy Forms, Django Markdownx

2. 开发环境搭建与项目初始化

2.1 环境准备与依赖安装

首先创建项目目录并设置虚拟环境:

# 创建项目目录
mkdir myblog && cd myblog# 创建虚拟环境
python -m venv venv# 激活虚拟环境
# Windows:
venv\Scripts\activate
# Linux/Mac:
source venv/bin/activate# 安装依赖
pip install django==4.2.0
pip install pillow  # 图片处理
pip install django-crispy-forms  # 表单美化
pip install django-markdownx  # Markdown支持
pip install gunicorn  # WSGI服务器# 生成requirements.txt
pip freeze > requirements.txt

2.2 创建Django项目和应用程序

# 创建Django项目
django-admin startproject blog_project .# 创建博客应用
python manage.py startapp blog# 创建用户认证应用
python manage.py startapp accounts

项目结构如下:

myblog/
├── blog_project/
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── blog/
│   ├── migrations/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── tests.py
│   ├── views.py
│   └── ...
├── accounts/
├── manage.py
└── requirements.txt

2.3 配置基础设置

编辑 blog_project/settings.py

"""
Django settings for blog_project
"""import os
from pathlib import Path# 构建路径:项目根目录
BASE_DIR = Path(__file__).resolve().parent.parent# 安全设置 - 在生产环境中需要修改!
SECRET_KEY = 'django-insecure-your-secret-key-here'
DEBUG = True
ALLOWED_HOSTS = []# 应用定义
INSTALLED_APPS = ['django.contrib.admin','django.contrib.auth','django.contrib.contenttypes','django.contrib.sessions','django.contrib.messages','django.contrib.staticfiles',# 第三方应用'crispy_forms','crispy_bootstrap5','markdownx',# 本地应用'blog','accounts',
]# 中间件
MIDDLEWARE = ['django.middleware.security.SecurityMiddleware','django.contrib.sessions.middleware.SessionMiddleware','django.middleware.common.CommonMiddleware','django.middleware.csrf.CsrfViewMiddleware','django.contrib.auth.middleware.AuthenticationMiddleware','django.contrib.messages.middleware.MessageMiddleware','django.middleware.clickjacking.XFrameOptionsMiddleware',
]# URL配置
ROOT_URLCONF = 'blog_project.urls'# 模板配置
TEMPLATES = [{'BACKEND': 'django.template.backends.django.DjangoTemplates','DIRS': [BASE_DIR / 'templates'],'APP_DIRS': True,'OPTIONS': {'context_processors': ['django.template.context_processors.debug','django.template.context_processors.request','django.contrib.auth.context_processors.auth','django.contrib.messages.context_processors.messages','blog.context_processors.categories_processor',  # 自定义上下文处理器],},},
]# WSGI配置
WSGI_APPLICATION = 'blog_project.wsgi.application'# 数据库配置
DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3','NAME': BASE_DIR / 'db.sqlite3',}
}# 密码验证
AUTH_PASSWORD_VALIDATORS = [{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',},{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',},{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',},{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',},
]# 国际化
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_TZ = True# 静态文件配置
STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / 'static']
STATIC_ROOT = BASE_DIR / 'staticfiles'# 媒体文件配置
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'# 默认主键字段类型
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'# 第三方应用配置
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"# 登录URL重定向
LOGIN_REDIRECT_URL = 'blog:post_list'
LOGOUT_REDIRECT_URL = 'blog:post_list'
LOGIN_URL = 'accounts:login'

3. 数据模型设计

3.1 核心模型关系分析

博客系统的核心数据模型包括文章(Post)、分类(Category)、标签(Tag)和评论(Comment)。它们之间的关系可以用以下模型表示:

Userstringusernamestringemailstringpassworddatetimedate_joinedbooleanis_activePoststringtitletextcontenttextexcerptstringslugdatetimecreated_atdatetimeupdated_atstringstatusintview_countCommenttextcontentdatetimecreated_atbooleanapprovedCategorystringnamestringslugtextdescriptionTagstringnamestringslugwriteswritescontainstagged_withhas

3.2 实现数据模型

编辑 blog/models.py

from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from django.urls import reverse
from django.utils.text import slugify
from markdownx.models import MarkdownxField
from markdownx.utils import markdownifyclass Category(models.Model):"""文章分类模型"""name = models.CharField('分类名称', max_length=100, unique=True)slug = models.SlugField('URL标识', max_length=100, unique=True)description = models.TextField('分类描述', blank=True)created_at = models.DateTimeField('创建时间', auto_now_add=True)class Meta:verbose_name = '分类'verbose_name_plural = '分类'ordering = ['name']def __str__(self):return self.namedef get_absolute_url(self):return reverse('blog:posts_by_category', args=[self.slug])def save(self, *args, **kwargs):if not self.slug:self.slug = slugify(self.name)super().save(*args, **kwargs)class Tag(models.Model):"""文章标签模型"""name = models.CharField('标签名称', max_length=50, unique=True)slug = models.SlugField('URL标识', max_length=50, unique=True)created_at = models.DateTimeField('创建时间', auto_now_add=True)class Meta:verbose_name = '标签'verbose_name_plural = '标签'ordering = ['name']def __str__(self):return self.namedef get_absolute_url(self):return reverse('blog:posts_by_tag', args=[self.slug])def save(self, *args, **kwargs):if not self.slug:self.slug = slugify(self.name)super().save(*args, **kwargs)class Post(models.Model):"""博客文章模型"""# 文章状态选项STATUS_CHOICES = (('draft', '草稿'),('published', '已发布'),)title = models.CharField('标题', max_length=200)slug = models.SlugField('URL标识', max_length=200, unique_for_date='created_at')content = MarkdownxField('内容')  # 使用Markdownx支持Markdownexcerpt = models.TextField('摘要', max_length=500, blank=True,help_text='可选的文章摘要,如果为空则自动生成')# 关系字段author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='作者', related_name='blog_posts')category = models.ForeignKey(Category, on_delete=models.SET_NULL,null=True, blank=True, verbose_name='分类',related_name='posts')tags = models.ManyToManyField(Tag, blank=True, verbose_name='标签',related_name='posts')# 时间字段created_at = models.DateTimeField('创建时间', default=timezone.now)updated_at = models.DateTimeField('更新时间', auto_now=True)published_at = models.DateTimeField('发布时间', null=True, blank=True)# 状态字段status = models.CharField('状态', max_length=10, choices=STATUS_CHOICES, default='draft')view_count = models.PositiveIntegerField('浏览次数', default=0)# SEO字段meta_description = models.CharField('Meta描述', max_length=300, blank=True,help_text='用于SEO的页面描述')meta_keywords = models.CharField('Meta关键词', max_length=200, blank=True,help_text='逗号分隔的关键词')class Meta:verbose_name = '文章'verbose_name_plural = '文章'ordering = ['-created_at']indexes = [models.Index(fields=['-created_at', 'status']),models.Index(fields=['slug', 'created_at']),]def __str__(self):return self.titledef get_absolute_url(self):"""获取文章绝对URL"""return reverse('blog:post_detail', args=[self.created_at.year,self.created_at.month,self.created_at.day,self.slug])def save(self, *args, **kwargs):"""保存前处理"""if not self.slug:self.slug = slugify(self.title)# 如果文章状态变为published且没有发布时间,设置发布时间if self.status == 'published' and not self.published_at:self.published_at = timezone.now()# 自动生成摘要if not self.excerpt:# 从Markdown内容中提取纯文本作为摘要plain_text = markdownify(self.content).strip()self.excerpt = plain_text[:197] + '...' if len(plain_text) > 200 else plain_textsuper().save(*args, **kwargs)def formatted_content(self):"""将Markdown内容转换为HTML"""return markdownify(self.content)def increase_views(self):"""增加浏览次数"""self.view_count += 1self.save(update_fields=['view_count'])class Comment(models.Model):"""文章评论模型"""post = models.ForeignKey(Post, on_delete=models.CASCADE, verbose_name='文章', related_name='comments')author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='评论者', related_name='comments')content = models.TextField('评论内容', max_length=1000)created_at = models.DateTimeField('评论时间', auto_now_add=True)updated_at = models.DateTimeField('更新时间', auto_now=True)approved = models.BooleanField('审核通过', default=False)# 父评论,支持回复功能parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, verbose_name='父评论',related_name='replies')class Meta:verbose_name = '评论'verbose_name_plural = '评论'ordering = ['-created_at']def __str__(self):return f'评论由 {self.author}{self.post}'def get_absolute_url(self):return self.post.get_absolute_url() + f'#comment-{self.id}'

3.3 创建数据库迁移并应用

# 创建迁移文件
python manage.py makemigrations# 应用迁移
python manage.py migrate# 创建超级用户
python manage.py createsuperuser

4. 管理后台配置

4.1 配置Django管理后台

编辑 blog/admin.py

from django.contrib import admin
from django.utils.html import format_html
from .models import Category, Tag, Post, Comment@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):"""分类管理"""list_display = ['name', 'slug', 'created_at', 'post_count']list_filter = ['created_at']search_fields = ['name', 'description']prepopulated_fields = {'slug': ['name']}def post_count(self, obj):return obj.posts.count()post_count.short_description = '文章数量'@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):"""标签管理"""list_display = ['name', 'slug', 'created_at', 'post_count']list_filter = ['created_at']search_fields = ['name']prepopulated_fields = {'slug': ['name']}def post_count(self, obj):return obj.posts.count()post_count.short_description = '文章数量'class CommentInline(admin.TabularInline):"""内联评论编辑"""model = Commentextra = 0fields = ['author', 'content', 'approved', 'created_at']readonly_fields = ['created_at']@admin.register(Post)
class PostAdmin(admin.ModelAdmin):"""文章管理"""list_display = ['title', 'author', 'category', 'status', 'created_at', 'updated_at', 'view_count', 'comment_count']list_filter = ['status', 'created_at', 'category', 'tags']search_fields = ['title', 'content', 'excerpt']prepopulated_fields = {'slug': ['title']}date_hierarchy = 'created_at'filter_horizontal = ['tags']# 字段分组显示fieldsets = (('基本信息', {'fields': ('title', 'slug', 'author', 'category', 'tags')}),('内容', {'fields': ('content', 'excerpt')}),('发布设置', {'fields': ('status', 'published_at')}),('SEO优化', {'fields': ('meta_description', 'meta_keywords'),'classes': ('collapse',)}),('统计信息', {'fields': ('view_count', 'created_at', 'updated_at'),'classes': ('collapse',)}),)readonly_fields = ['created_at', 'updated_at']def comment_count(self, obj):return obj.comments.count()comment_count.short_description = '评论数'def save_model(self, request, obj, form, change):"""保存模型时自动设置作者"""if not obj.author_id:obj.author = request.usersuper().save_model(request, obj, form, change)@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):"""评论管理"""list_display = ['author', 'post', 'content_preview', 'created_at', 'approved']list_filter = ['approved', 'created_at']search_fields = ['author__username', 'content', 'post__title']actions = ['approve_comments', 'disapprove_comments']def content_preview(self, obj):return obj.content[:50] + '...' if len(obj.content) > 50 else obj.contentcontent_preview.short_description = '评论内容'def approve_comments(self, request, queryset):"""批准评论操作"""queryset.update(approved=True)self.message_user(request, f'已批准 {queryset.count()} 条评论')approve_comments.short_description = '批准选中的评论'def disapprove_comments(self, request, queryset):"""取消批准评论操作"""queryset.update(approved=False)self.message_user(request, f'已取消批准 {queryset.count()} 条评论')disapprove_comments.short_description = '取消批准选中的评论'

5. 视图和URL配置

5.1 博客视图实现

编辑 blog/views.py

from django.shortcuts import render, get_object_or_404, redirect
from django.views.generic import ListView, DetailView, CreateView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from django.db.models import Q, Count
from django.utils import timezone
from .models import Post, Category, Tag, Comment
from .forms import CommentFormclass PostListView(ListView):"""文章列表视图"""model = Posttemplate_name = 'blog/post_list.html'context_object_name = 'posts'paginate_by = 10def get_queryset(self):"""获取已发布的文章"""queryset = Post.objects.filter(status='published',published_at__lte=timezone.now()).select_related('author', 'category').prefetch_related('tags')# 搜索功能query = self.request.GET.get('q')if query:queryset = queryset.filter(Q(title__icontains=query) |Q(content__icontains=query) |Q(excerpt__icontains=query) |Q(category__name__icontains=query) |Q(tags__name__icontains=query)).distinct()return querysetdef get_context_data(self, **kwargs):"""添加上下文数据"""context = super().get_context_data(**kwargs)context['search_query'] = self.request.GET.get('q', '')# 热门文章(按浏览量)context['popular_posts'] = Post.objects.filter(status='published').order_by('-view_count')[:5]return contextclass PostDetailView(DetailView):"""文章详情视图"""model = Posttemplate_name = 'blog/post_detail.html'context_object_name = 'post'def get_queryset(self):"""获取已发布的文章"""return Post.objects.filter(status='published',published_at__lte=timezone.now())def get_object(self, queryset=None):"""获取文章对象并增加浏览次数"""obj = super().get_object(queryset)obj.increase_views()return objdef get_context_data(self, **kwargs):"""添加上下文数据"""context = super().get_context_data(**kwargs)# 添加评论表单context['comment_form'] = CommentForm()# 获取已批准的评论context['comments'] = self.object.comments.filter(approved=True, parent__isnull=True  # 只获取顶级评论).select_related('author')# 相关文章(同分类或同标签)post = self.objectrelated_posts = Post.objects.filter(status='published',published_at__lte=timezone.now()).filter(Q(category=post.category) |Q(tags__in=post.tags.all())).exclude(id=post.id).distinct()[:5]context['related_posts'] = related_postsreturn contextclass PostsByCategoryView(ListView):"""按分类查看文章列表"""model = Posttemplate_name = 'blog/posts_by_category.html'context_object_name = 'posts'paginate_by = 10def get_queryset(self):"""获取指定分类的已发布文章"""self.category = get_object_or_404(Category, slug=self.kwargs['slug'])return Post.objects.filter(category=self.category,status='published',published_at__lte=timezone.now()).select_related('author', 'category').prefetch_related('tags')def get_context_data(self, **kwargs):"""添加上下文数据"""context = super().get_context_data(**kwargs)context['category'] = self.categoryreturn contextclass PostsByTagView(ListView):"""按标签查看文章列表"""model = Posttemplate_name = 'blog/posts_by_tag.html'context_object_name = 'posts'paginate_by = 10def get_queryset(self):"""获取指定标签的已发布文章"""self.tag = get_object_or_404(Tag, slug=self.kwargs['slug'])return Post.objects.filter(tags=self.tag,status='published',published_at__lte=timezone.now()).select_related('author', 'category').prefetch_related('tags')def get_context_data(self, **kwargs):"""添加上下文数据"""context = super().get_context_data(**kwargs)context['tag'] = self.tagreturn contextclass CommentCreateView(LoginRequiredMixin, CreateView):"""创建评论视图"""model = Commentform_class = CommentFormdef form_valid(self, form):"""表单验证通过后的处理"""post = get_object_or_404(Post, slug=self.kwargs['slug'],created_at__year=self.kwargs['year'],created_at__month=self.kwargs['month'],created_at__day=self.kwargs['day'])comment = form.save(commit=False)comment.post = postcomment.author = self.request.user# 设置父评论(如果是回复)parent_id = self.request.POST.get('parent_id')if parent_id:try:comment.parent = Comment.objects.get(id=parent_id)except Comment.DoesNotExist:passcomment.save()messages.success(self.request, '评论已提交,等待审核!')return redirect(post.get_absolute_url())def about(request):"""关于页面"""return render(request, 'blog/about.html')# 自定义上下文处理器
def categories_processor(request):"""在所有模板中提供分类数据"""categories = Category.objects.annotate(post_count=Count('posts')).filter(post_count__gt=0)tags = Tag.objects.annotate(post_count=Count('posts')).filter(post_count__gt=0)return {'categories': categories,'tags': tags,}

5.2 用户认证视图

编辑 accounts/views.py

from django.shortcuts import render, redirect
from django.contrib.auth.forms import UserCreationForm
from django.contrib import messages
from django.contrib.auth.decorators import login_requireddef register(request):"""用户注册视图"""if request.method == 'POST':form = UserCreationForm(request.POST)if form.is_valid():form.save()username = form.cleaned_data.get('username')messages.success(request, f'账号 {username} 创建成功!请登录。')return redirect('accounts:login')else:form = UserCreationForm()return render(request, 'accounts/register.html', {'form': form})@login_required
def profile(request):"""用户个人资料页面"""user_posts = request.user.blog_posts.filter(status='published')return render(request, 'accounts/profile.html', {'user_posts': user_posts})

5.3 URL路由配置

创建 blog/urls.py

from django.urls import path
from . import viewsapp_name = 'blog'urlpatterns = [path('', views.PostListView.as_view(), name='post_list'),path('about/', views.about, name='about'),path('category/<slug:slug>/', views.PostsByCategoryView.as_view(), name='posts_by_category'),path('tag/<slug:slug>/', views.PostsByTagView.as_view(), name='posts_by_tag'),path('<int:year>/<int:month>/<int:day>/<slug:slug>/', views.PostDetailView.as_view(), name='post_detail'),path('<int:year>/<int:month>/<int:day>/<slug:slug>/comment/', views.CommentCreateView.as_view(), name='add_comment'),
]

创建 accounts/urls.py

from django.urls import path
from django.contrib.auth import views as auth_views
from . import viewsapp_name = 'accounts'urlpatterns = [path('register/', views.register, name='register'),path('login/', auth_views.LoginView.as_view(template_name='accounts/login.html'), name='login'),path('logout/', auth_views.LogoutView.as_view(), name='logout'),path('profile/', views.profile, name='profile'),
]

配置主URL路由 blog_project/urls.py

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import staticurlpatterns = [path('admin/', admin.site.urls),path('markdownx/', include('markdownx.urls')),path('accounts/', include('accounts.urls')),path('', include('blog.urls')),
]# 开发环境下的媒体文件服务
if settings.DEBUG:urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

6. 表单和模板系统

6.1 评论表单实现

创建 blog/forms.py

from django import forms
from .models import Commentclass CommentForm(forms.ModelForm):"""评论表单"""class Meta:model = Commentfields = ['content', 'parent']widgets = {'content': forms.Textarea(attrs={'class': 'form-control','rows': 4,'placeholder': '写下你的评论...'}),'parent': forms.HiddenInput(),  # 隐藏父评论字段}labels = {'content': '',}def clean_content(self):"""评论内容验证"""content = self.cleaned_data.get('content')if len(content.strip()) < 5:raise forms.ValidationError('评论内容至少需要5个字符')return content

6.2 基础模板设计

创建 templates/base.html

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>{% block title %}我的个人博客{% endblock %}</title><!-- Bootstrap 5 CSS --><link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"><!-- 自定义CSS --><style>.navbar-brand {font-weight: bold;}.post-card {transition: transform 0.2s;}.post-card:hover {transform: translateY(-2px);box-shadow: 0 4px 8px rgba(0,0,0,0.1);}.sidebar-widget {background: #f8f9fa;border-radius: 8px;padding: 1.5rem;margin-bottom: 1.5rem;}</style>{% block extra_css %}{% endblock %}
</head>
<body><!-- 导航栏 --><nav class="navbar navbar-expand-lg navbar-dark bg-dark"><div class="container"><a class="navbar-brand" href="{% url 'blog:post_list' %}">我的博客</a><button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse" id="navbarNav"><ul class="navbar-nav me-auto"><li class="nav-item"><a class="nav-link" href="{% url 'blog:post_list' %}">首页</a></li><li class="nav-item"><a class="nav-link" href="{% url 'blog:about' %}">关于</a></li></ul><!-- 搜索表单 --><form class="d-flex me-3" action="{% url 'blog:post_list' %}" method="get"><input class="form-control me-2" type="search" name="q" placeholder="搜索文章..." value="{{ request.GET.q }}"><button class="btn btn-outline-light" type="submit">搜索</button></form><!-- 用户认证链接 --><ul class="navbar-nav">{% if user.is_authenticated %}<li class="nav-item dropdown"><a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">{{ user.username }}</a><ul class="dropdown-menu"><li><a class="dropdown-item" href="{% url 'accounts:profile' %}">个人资料</a></li>{% if user.is_staff %}<li><a class="dropdown-item" href="{% url 'admin:index' %}">管理后台</a></li>{% endif %}<li><hr class="dropdown-divider"></li><li><a class="dropdown-item" href="{% url 'accounts:logout' %}">退出</a></li></ul></li>{% else %}<li class="nav-item"><a class="nav-link" href="{% url 'accounts:login' %}">登录</a></li><li class="nav-item"><a class="nav-link" href="{% url 'accounts:register' %}">注册</a></li>{% endif %}</ul></div></div></nav><!-- 消息提示 --><div class="container mt-3">{% if messages %}{% for message in messages %}<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">{{ message }}<button type="button" class="btn-close" data-bs-dismiss="alert"></button></div>{% endfor %}{% endif %}</div><!-- 主要内容 --><main class="container my-4"><div class="row"><!-- 主内容区域 --><div class="col-lg-8">{% block content %}{% endblock %}</div><!-- 侧边栏 --><div class="col-lg-4">{% include 'blog/sidebar.html' %}</div></div></main><!-- 页脚 --><footer class="bg-dark text-light py-4 mt-5"><div class="container text-center"><p>&copy; 2024 我的个人博客. 基于Django构建.</p></div></footer><!-- Bootstrap JS --><script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>{% block extra_js %}{% endblock %}
</body>
</html>

6.3 文章列表模板

创建 templates/blog/post_list.html

{% extends 'base.html' %}{% block title %}首页 - 我的个人博客{% endblock %}{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4"><h1>最新文章</h1>{% if search_query %}<span class="text-muted">搜索: "{{ search_query }}"</span>{% endif %}
</div>{% if posts %}{% for post in posts %}<article class="card post-card mb-4"><div class="card-body"><h2 class="card-title"><a href="{{ post.get_absolute_url }}" class="text-decoration-none">{{ post.title }}</a></h2><div class="text-muted mb-3"><small><i class="fas fa-user"></i> {{ post.author.username }} |<i class="fas fa-calendar"></i> {{ post.created_at|date:"Y年m月d日" }} |<i class="fas fa-eye"></i> {{ post.view_count }} 次浏览 |{% if post.category %}<i class="fas fa-folder"></i> <a href="{{ post.category.get_absolute_url }}" class="text-muted">{{ post.category.name }}</a>{% endif %}</small></div>{% if post.tags.exists %}<div class="mb-3">{% for tag in post.tags.all %}<a href="{{ tag.get_absolute_url }}" class="badge bg-secondary text-decoration-none">{{ tag.name }}</a>{% endfor %}</div>{% endif %}<p class="card-text">{{ post.excerpt }}</p><a href="{{ post.get_absolute_url }}" class="btn btn-primary"></a></div></article>{% endfor %}<!-- 分页 -->{% if is_paginated %}<nav aria-label="文章分页"><ul class="pagination justify-content-center">{% if page_obj.has_previous %}<li class="page-item"><a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&q={{ search_query }}{% endif %}">上一页</a></li>{% endif %}{% for num in page_obj.paginator.page_range %}<li class="page-item {% if page_obj.number == num %}active{% endif %}"><a class="page-link" href="?page={{ num }}{% if search_query %}&q={{ search_query }}{% endif %}">{{ num }}</a></li>{% endfor %}{% if page_obj.has_next %}<li class="page-item"><a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&q={{ search_query }}{% endif %}">下一页</a></li>{% endif %}</ul></nav>{% endif %}{% else %}<div class="text-center py-5"><h3>暂无文章</h3><p class="text-muted">{% if search_query %}没有找到相关文章{% else %}博主正在努力写作中...{% endif %}</p></div>
{% endif %}
{% endblock %}

6.4 文章详情模板

创建 templates/blog/post_detail.html

{% extends 'base.html' %}
{% load markdownify %}{% block title %}{{ post.title }} - 我的个人博客{% endblock %}{% block content %}
<article><!-- 文章头部 --><header class="mb-4"><h1 class="display-4">{{ post.title }}</h1><div class="text-muted mb-3"><small><i class="fas fa-user"></i> {{ post.author.username }} |<i class="fas fa-calendar"></i> {{ post.created_at|date:"Y年m月d日" }} |<i class="fas fa-eye"></i> {{ post.view_count }} 次浏览 |{% if post.category %}<i class="fas fa-folder"></i> <a href="{{ post.category.get_absolute_url }}" class="text-muted">{{ post.category.name }}</a>{% endif %}</small></div>{% if post.tags.exists %}<div class="mb-3">{% for tag in post.tags.all %}<a href="{{ tag.get_absolute_url }}" class="badge bg-primary text-decoration-none">{{ tag.name }}</a>{% endfor %}</div>{% endif %}</header><!-- 文章内容 --><div class="post-content mb-5">{{ post.content|markdownify }}</div><!-- 相关文章 -->{% if related_posts %}<section class="related-posts mb-5"><h3>相关文章</h3><div class="row">{% for related_post in related_posts %}<div class="col-md-6"><div class="card mb-3"><div class="card-body"><h5 class="card-title"><a href="{{ related_post.get_absolute_url }}" class="text-decoration-none">{{ related_post.title }}</a></h5><p class="card-text text-muted small">{{ related_post.created_at|date:"Y-m-d" }}</p></div></div></div>{% endfor %}</div></section>{% endif %}<!-- 评论区域 --><section class="comments"><h3>评论 ({{ comments.count }})</h3><!-- 评论表单 -->{% if user.is_authenticated %}<div class="card mb-4"><div class="card-body"><h5 class="card-title">发表评论</h5><form method="post" action="{% url 'blog:add_comment' post.created_at.year post.created_at.month post.created_at.day post.slug %}">{% csrf_token %}{{ comment_form.content }}<button type="submit" class="btn btn-primary mt-3">提交评论</button></form></div></div>{% else %}<div class="alert alert-info"><a href="{% url 'accounts:login' %}">登录</a>后发表评论</div>{% endif %}<!-- 评论列表 --><div class="comments-list">{% for comment in comments %}<div class="card mb-3" id="comment-{{ comment.id }}"><div class="card-body"><div class="d-flex justify-content-between align-items-start"><h6 class="card-title">{{ comment.author.username }}</h6><small class="text-muted">{{ comment.created_at|date:"Y-m-d H:i" }}</small></div><p class="card-text">{{ comment.content }}</p><!-- 回复按钮 -->{% if user.is_authenticated %}<button class="btn btn-sm btn-outline-secondary reply-btn" data-comment-id="{{ comment.id }}">回复</button>{% endif %}<!-- 回复列表 -->{% for reply in comment.replies.all %}{% if reply.approved %}<div class="card mt-3 ms-4"><div class="card-body"><div class="d-flex justify-content-between align-items-start"><h6 class="card-title">{{ reply.author.username }}</h6><small class="text-muted">{{ reply.created_at|date:"Y-m-d H:i" }}</small></div><p class="card-text">{{ reply.content }}</p></div></div>{% endif %}{% endfor %}</div></div>{% empty %}<div class="text-center py-4"><p class="text-muted">暂无评论,快来抢沙发吧!</p></div>{% endfor %}</div></section>
</article>
{% endblock %}{% block extra_js %}
<script>
// 回复功能
document.addEventListener('DOMContentLoaded', function() {const replyButtons = document.querySelectorAll('.reply-btn');replyButtons.forEach(button => {button.addEventListener('click', function() {const commentId = this.dataset.commentId;const commentForm = document.querySelector('form');const contentTextarea = commentForm.querySelector('textarea');// 设置回复内容占位符contentTextarea.placeholder = `回复评论 #${commentId}...`;// 添加父评论ID到表单let parentInput = commentForm.querySelector('input[name="parent_id"]');if (!parentInput) {parentInput = document.createElement('input');parentInput.type = 'hidden';parentInput.name = 'parent_id';commentForm.appendChild(parentInput);}parentInput.value = commentId;// 滚动到评论表单commentForm.scrollIntoView({ behavior: 'smooth' });contentTextarea.focus();});});
});
</script>
{% endblock %}

6.5 侧边栏模板

创建 templates/blog/sidebar.html

<!-- 关于我 -->
<div class="sidebar-widget"><h4>关于博客</h4><p class="mb-0">欢迎来到我的个人博客,这里分享技术心得和生活感悟。</p>
</div><!-- 分类 -->
<div class="sidebar-widget"><h4>文章分类</h4><ul class="list-unstyled">{% for category in categories %}<li class="mb-2"><a href="{{ category.get_absolute_url }}" class="text-decoration-none">{{ category.name }}<span class="badge bg-secondary float-end">{{ category.post_count }}</span></a></li>{% empty %}<li class="text-muted">暂无分类</li>{% endfor %}</ul>
</div><!-- 标签云 -->
<div class="sidebar-widget"><h4>热门标签</h4><div class="tag-cloud">{% for tag in tags %}<a href="{{ tag.get_absolute_url }}" class="badge bg-light text-dark text-decoration-none me-1 mb-1">{{ tag.name }}</a>{% empty %}<p class="text-muted">暂无标签</p>{% endfor %}</div>
</div><!-- 热门文章 -->
<div class="sidebar-widget"><h4>热门文章</h4><ul class="list-unstyled">{% for post in popular_posts %}<li class="mb-2"><a href="{{ post.get_absolute_url }}" class="text-decoration-none small">{{ post.title }}</a></li>{% empty %}<li class="text-muted small">暂无热门文章</li>{% endfor %}</ul>
</div>

7. 完整项目部署配置

7.1 生产环境设置

创建 blog_project/settings_prod.py

"""
生产环境设置
"""
import os
from .settings import *# 安全设置
DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com', 'localhost']# 数据库配置 - 使用PostgreSQL
DATABASES = {'default': {'ENGINE': 'django.db.backends.postgresql','NAME': os.getenv('DB_NAME', 'blog_db'),'USER': os.getenv('DB_USER', 'blog_user'),'PASSWORD': os.getenv('DB_PASSWORD', ''),'HOST': os.getenv('DB_HOST', 'localhost'),'PORT': os.getenv('DB_PORT', '5432'),}
}# 静态文件配置
STATIC_ROOT = '/var/www/static/'
MEDIA_ROOT = '/var/www/media/'# 安全中间件
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True# HSTS设置
SECURE_HSTS_SECONDS = 31536000  # 1年
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True# 日志配置
LOGGING = {'version': 1,'disable_existing_loggers': False,'handlers': {'file': {'level': 'ERROR','class': 'logging.FileHandler','filename': '/var/log/django/error.log',},},'loggers': {'django': {'handlers': ['file'],'level': 'ERROR','propagate': True,},},
}

7.2 Gunicorn配置

创建 gunicorn.conf.py

# Gunicorn配置文件
bind = "0.0.0.0:8000"
workers = 3
worker_class = "sync"
worker_connections = 1000
timeout = 30
max_requests = 1000
max_requests_jitter = 100
preload_app = True

7.3 部署脚本

创建 deploy.sh

#!/bin/bash# 部署脚本
echo "开始部署Django博客系统..."# 激活虚拟环境
source venv/bin/activate# 安装依赖
pip install -r requirements.txt# 收集静态文件
python manage.py collectstatic --noinput# 数据库迁移
python manage.py migrate# 重启Gunicorn
sudo systemctl restart gunicorn# 重启Nginx
sudo systemctl restart nginxecho "部署完成!"

7.4 Nginx配置

创建 /etc/nginx/sites-available/blog

server {listen 80;server_name yourdomain.com www.yourdomain.com;# 静态文件location /static/ {alias /var/www/static/;expires 30d;add_header Cache-Control "public, immutable";}# 媒体文件location /media/ {alias /var/www/media/;expires 30d;}# Django应用location / {proxy_pass http://127.0.0.1:8000;proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;proxy_set_header X-Forwarded-Proto $scheme;}# 安全头部add_header X-Frame-Options "SAMEORIGIN" always;add_header X-XSS-Protection "1; mode=block" always;add_header X-Content-Type-Options "nosniff" always;add_header Referrer-Policy "no-referrer-when-downgrade" always;
}

8. 代码自查与优化

8.1 安全自查

# security_check.py
"""
安全自查脚本
"""
from django.core.checks import register, Tags@register(Tags.security)
def check_security_settings(app_configs, **kwargs):"""检查安全设置"""errors = []from django.conf import settings# 检查DEBUG模式if settings.DEBUG:errors.append('security.W001','DEBUG模式在生产环境中应该设置为False')# 检查密钥if settings.SECRET_KEY.startswith('django-insecure-'):errors.append('security.W002','应该使用强密码作为SECRET_KEY')# 检查允许的主机if not settings.ALLOWED_HOSTS:errors.append('security.W003','ALLOWED_HOSTS应该包含你的域名')return errors

8.2 性能优化

# blog/optimizations.py
"""
性能优化工具
"""
from django.db.models import Prefetchdef optimize_post_queryset(queryset):"""优化文章查询集"""return queryset.select_related('author', 'category').prefetch_related(Prefetch('tags', queryset=Tag.objects.only('name', 'slug')),Prefetch('comments', queryset=Comment.objects.filter(approved=True).select_related('author').only('author__username', 'content', 'created_at')))def get_cached_categories():"""获取缓存的分类数据"""from django.core.cache import cachefrom .models import Categorycache_key = 'blog_categories'categories = cache.get(cache_key)if categories is None:categories = Category.objects.annotate(post_count=Count('posts')).filter(post_count__gt=0)cache.set(cache_key, categories, 3600)  # 缓存1小时return categories

8.3 完整的测试套件

创建 blog/tests.py

from django.test import TestCase, Client
from django.contrib.auth.models import User
from django.urls import reverse
from .models import Post, Category, Tag, Commentclass BlogTestCase(TestCase):"""博客系统测试用例"""def setUp(self):"""测试数据准备"""self.client = Client()self.user = User.objects.create_user(username='testuser',password='testpass123')self.category = Category.objects.create(name='测试分类',slug='test-category')self.post = Post.objects.create(title='测试文章',slug='test-post',content='测试内容',author=self.user,category=self.category,status='published')def test_post_creation(self):"""测试文章创建"""self.assertEqual(self.post.title, '测试文章')self.assertEqual(self.post.status, 'published')self.assertEqual(str(self.post), '测试文章')def test_post_list_view(self):"""测试文章列表视图"""response = self.client.get(reverse('blog:post_list'))self.assertEqual(response.status_code, 200)self.assertContains(response, '测试文章')def test_post_detail_view(self):"""测试文章详情视图"""response = self.client.get(self.post.get_absolute_url())self.assertEqual(response.status_code, 200)self.assertContains(response, '测试文章')def test_comment_creation(self):"""测试评论创建"""comment = Comment.objects.create(post=self.post,author=self.user,content='测试评论内容')self.assertEqual(comment.content, '测试评论内容')self.assertFalse(comment.approved)  # 新评论默认未审核def test_search_functionality(self):"""测试搜索功能"""response = self.client.get(reverse('blog:post_list'), {'q': '测试'})self.assertEqual(response.status_code, 200)self.assertContains(response, '测试文章')

9. 项目总结与扩展建议

9.1 项目成果

通过本教程,我们成功构建了一个功能完整的Django博客系统,具备以下特性:

  • ✅ 用户认证系统(注册、登录、个人资料)
  • ✅ 完整的文章管理系统(CRUD操作)
  • ✅ 分类和标签系统
  • ✅ 评论和回复功能
  • ✅ 搜索功能
  • ✅ Markdown支持
  • ✅ 响应式设计
  • ✅ 生产环境部署配置

9.2 扩展功能建议

基础博客系统
内容扩展
性能优化
用户体验
多语言支持
文章系列
附件管理
Redis缓存
CDN加速
数据库优化
暗黑模式
阅读进度
社交分享

9.3 进一步学习路径

  1. 进阶Django特性

    • Django REST Framework构建API
    • Django Channels实现WebSocket
    • Celery处理异步任务
  2. 前端技术栈

    • 集成Vue.js或React
    • 使用Webpack构建前端资源
    • PWA(渐进式Web应用)支持
  3. DevOps技能

    • Docker容器化部署
    • CI/CD流水线
    • 监控和日志分析

这个博客系统不仅是一个可用的产品,更是学习Django全栈开发的绝佳起点。通过不断扩展和完善功能,你将掌握现代Web开发的核心技能,为更复杂的项目打下坚实基础。

记住:最好的学习方式是在实践中不断迭代和改进。现在就开始你的博客之旅吧!

http://www.dtcms.com/a/426657.html

相关文章:

  • 工业互联网的云原生转型路径
  • 做网站需要哪些素材建筑工程 技术支持 东莞网站建设
  • Spring Boot 缓存技术
  • AI应用生成平台:数据库、缓存与存储
  • J2Cache 多级缓存配置与使用
  • 【JAVA】【BUG】经常出现的典型 bug 及解决办法
  • 做网站怎么存放视频哪个公司的室内设计公司
  • GitHub 热榜项目 - 日榜(2025-09-30)
  • Microsoft Fabric - 尝试一下Workspace中的Deployment pipeline
  • 区块链论文速读 CCF A--WWW 2025(6)
  • Bug——PaddleX人脸识别报错:Process finished with exit code -1073741819 (0xC0000005)
  • SSM基于的宠物领养管理系统ugssn(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。
  • 四种对象型创建模式:抽象工厂、 build模式、原型ProtoType与单例模式
  • 即墨网站建设公司旅游网站建设电子商务的困惑
  • 央视优购物官方网站网页制作与网站管理
  • 本地多版本 Node.js 切换指南:解决 Vue nodejs 等项目版本冲突问题
  • 阿里云 AI 中间件重磅发布,打通 AI 应用落地“最后一公里”
  • 全面解析Umi-OCR手写体识别能力:开源OCR的新标杆
  • Spring Boot整合Kafka:解决消息挤压、丢失与重复消费
  • 【系统架构师-案例分析】2025年5月份案例分析第一题-架构评估
  • OpenHarmony之Histreamer引擎深度解析:pipeline_core架构如何全面取代GStreamer,一统音视频播放与录制
  • 个人简历html代码山西seo推广方案
  • ARM芯片架构之coresight 时间戳组件介绍
  • LeetCode算法日记 - Day 58: 目标和、数组总和
  • 在不同开发语言与场景下设计模式的使用
  • 服务机构电子商务网站有哪些软件外包公司开发流程
  • 微软 2025 年 8 月更新:对固态硬盘与电脑功能有哪些潜在的影响
  • VB6 ADO没有轻量级内存数据库吗?类似SQLITE
  • 微软Windows原罪不可原谅
  • 微软警示AI驱动的钓鱼攻击:LLM生成的SVG文件绕过邮件安全检测