URL 路由与文章详情页
本章你将学会 Django 的动态路由配置,实现文章详情页和页面之间的链接跳转。
动态路由是什么?
首页展示的是所有文章,每篇文章需要有自己的独立详情页。
URL 应该像这样:/post/1/、/post/2/,其中数字部分是文章的主键(pk)。
动态路由 允许 URL 中包含变量,Django 会自动提取并传入视图函数。
配置动态路由
实例
# 文件路径:blog/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
# int:pk — URL 中的整数参数,名为 pk(primary key)
# name='post_detail' — 给路由命名,模板中用 {% url %} 引用
path('post/<int:pk>/', views.post_detail, name='post_detail'),
]
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
# int:pk — URL 中的整数参数,名为 pk(primary key)
# name='post_detail' — 给路由命名,模板中用 {% url %} 引用
path('post/<int:pk>/', views.post_detail, name='post_detail'),
]
路由转换器说明:
| 转换器 | 示例路径 | 匹配规则 |
|---|---|---|
<int:pk> | /post/3/ | 匹配正整数 |
<str:slug> | /post/django-guide/ | 匹配字符串(不含 /) |
<slug:slug> | /post/django-guide/ | 匹配字母、数字、-、_ |
<uuid:id> | /post/uuid/ | 匹配 UUID 格式 |
编写详情页视图
详情页需要根据 URL 中的 pk 从数据库查询对应的文章。
实例
# 文件路径:blog/views.py
from django.shortcuts import render, get_object_or_404
from .models import Post
def post_detail(request, pk):
"""文章详情页"""
# get_object_or_404:查询单条记录
# 找到就返回对象,找不到自动返回 404 页面(不写 try/except)
post = get_object_or_404(Post, pk=pk)
context = {
'post': post,
'title': f'{post.title} - RUNOOB 博客'
}
return render(request, 'blog/post_detail.html', context)
from django.shortcuts import render, get_object_or_404
from .models import Post
def post_detail(request, pk):
"""文章详情页"""
# get_object_or_404:查询单条记录
# 找到就返回对象,找不到自动返回 404 页面(不写 try/except)
post = get_object_or_404(Post, pk=pk)
context = {
'post': post,
'title': f'{post.title} - RUNOOB 博客'
}
return render(request, 'blog/post_detail.html', context)
get_object_or_404是 Django 的最佳实践。它等价于Post.objects.get(pk=pk)+try/except Post.DoesNotExist,但代码更简洁。用户访问不存在的文章 ID 时会看到友好的 404 页面,而非报错。
创建详情页模板
实例
<!-- 文件路径:blog/templates/blog/post_detail.html -->
{% extends 'blog/base.html' %}
{% block title %}{{ post.title }} - RUNOOB 博客{% endblock %}
{% block content %}
<article class="post-view">
<span class="category-tag">{{ post.category.name }}</span>
<h1>{{ post.title }}</h1>
<time>{{ post.created_at|date:"Y-m-d" }}</time>
<!-- 正文使用 |safe 过滤器渲染 HTML(Admin 中输入的 HTML 内容) -->
<div class="content">
{{ post.content|safe }}
</div>
<a href="{% url 'index' %}" class="back-link">← 返回首页</a>
</article>
{% endblock %}
{% extends 'blog/base.html' %}
{% block title %}{{ post.title }} - RUNOOB 博客{% endblock %}
{% block content %}
<article class="post-view">
<span class="category-tag">{{ post.category.name }}</span>
<h1>{{ post.title }}</h1>
<time>{{ post.created_at|date:"Y-m-d" }}</time>
<!-- 正文使用 |safe 过滤器渲染 HTML(Admin 中输入的 HTML 内容) -->
<div class="content">
{{ post.content|safe }}
</div>
<a href="{% url 'index' %}" class="back-link">← 返回首页</a>
</article>
{% endblock %}
详情页样式
实例
/* 在 base.html 的 style 中追加 */
.post-view {
max-width: 720px;
margin: 0 auto;
}
.category-tag {
display: inline-block;
padding: 4px 12px;
background: #e8f5e9;
color: #2e7d32;
border-radius: 12px;
font-size: 13px;
margin-bottom: 12px;
}
.post-view h1 {
font-size: 32px;
margin-bottom: 12px;
line-height: 1.4;
}
.post-view time {
display: block;
color: #999;
font-size: 14px;
margin-bottom: 30px;
}
.content {
line-height: 1.8;
font-size: 16px;
color: #333;
}
.content h2 {
margin: 24px 0 12px;
font-size: 22px;
}
.content p {
margin-bottom: 12px;
}
.back-link {
display: inline-block;
margin-top: 40px;
color: #2c3e50;
text-decoration: none;
}
.post-view {
max-width: 720px;
margin: 0 auto;
}
.category-tag {
display: inline-block;
padding: 4px 12px;
background: #e8f5e9;
color: #2e7d32;
border-radius: 12px;
font-size: 13px;
margin-bottom: 12px;
}
.post-view h1 {
font-size: 32px;
margin-bottom: 12px;
line-height: 1.4;
}
.post-view time {
display: block;
color: #999;
font-size: 14px;
margin-bottom: 30px;
}
.content {
line-height: 1.8;
font-size: 16px;
color: #333;
}
.content h2 {
margin: 24px 0 12px;
font-size: 22px;
}
.content p {
margin-bottom: 12px;
}
.back-link {
display: inline-block;
margin-top: 40px;
color: #2c3e50;
text-decoration: none;
}
|safe过滤器告诉 Django:这段内容是安全的 HTML,直接渲染,不要转义。使用 safe 时要确保内容是可信的(如你自己在 Admin 中编辑的内容)。如果是用户提交的内容,绝对不能加 safe,否则有 XSS 风险。
模板中生成链接:{% url %}
在首页的文章卡片上添加详情页链接。
实例
<!-- 修改 blog/templates/blog/index.html 中的文章卡片 -->
<div class="article-grid">
{% for post in posts %}
<a href="{% url 'post_detail' post.pk %}" class="card-link">
<div class="article-card">
<div class="card-content">
<span class="card-category">{{ post.category.name }}</span>
<h3>{{ post.title }}</h3>
<p>{{ post.summary|truncatechars:80 }}</p>
<span class="card-date">{{ post.created_at|date:"Y-m-d" }}</span>
</div>
</div>
</a>
{% endfor %}
</div>
<div class="article-grid">
{% for post in posts %}
<a href="{% url 'post_detail' post.pk %}" class="card-link">
<div class="article-card">
<div class="card-content">
<span class="card-category">{{ post.category.name }}</span>
<h3>{{ post.title }}</h3>
<p>{{ post.summary|truncatechars:80 }}</p>
<span class="card-date">{{ post.created_at|date:"Y-m-d" }}</span>
</div>
</div>
</a>
{% endfor %}
</div>
{% url 'post_detail' post.pk %} 会根据路由名称和参数自动生成正确的 URL。
如果以后路由变了(如 /post/ 改成 /article/),模板中的链接会自动更新,不需要手动修改。
模型添加 get_absolute_url()
在 Post 模型中定义此方法,可以让 Admin 后台、Django 内置工具等自动找到详情页链接。
实例
# 文件路径:blog/models.py 的 Post 类中添加
from django.urls import reverse
class Post(models.Model):
# ... 字段定义 ...
def __str__(self):
return self.title
def get_absolute_url(self):
# reverse 根据路由名称和参数反查 URL
return reverse('post_detail', kwargs={'pk': self.pk})
from django.urls import reverse
class Post(models.Model):
# ... 字段定义 ...
def __str__(self):
return self.title
def get_absolute_url(self):
# reverse 根据路由名称和参数反查 URL
return reverse('post_detail', kwargs={'pk': self.pk})
本章小结
本章你掌握了 Django 路由的核心:动态路由 <int:pk> 传递参数、get_object_or_404 安全查询、{% url %} 模板标签生成链接、reverse 反查 URL。
现在博客有了首页列表和文章详情页两个页面,点击卡片可以跳转阅读全文。
