Today’s goal
Make each post title clickable. The link will contain the post’s ID, and Django will use that ID to find and display one complete article.
Example:
/posts/3/ → find post with id 3 → show post_detail.html
Step 1 — Add the detail URL
Open blog/urls.py and add a second path:
from django.urls import path
from . import views
urlpatterns = [
path("", views.home, name="home"),
path("posts/<int:pk>/", views.post_detail, name="post_detail"),
]
<int:pk> means:
- this part of the URL must be an integer;
- call that integer
pk; - pass it to the view.
Visiting /posts/3/ will call post_detail(request, pk=3).
Step 2 — Add the detail view
Change the shortcuts import at the top of blog/views.py:
from django.shortcuts import get_object_or_404, render
Keep home() and add this function below it:
def post_detail(request, pk):
post = get_object_or_404(Post, pk=pk)
context = {
"post": post,
}
return render(request, "blog/post_detail.html", context)
get_object_or_404 tries to find the post. If the ID does not exist, Django shows a normal 404 page instead of crashing.
Step 3 — Link each title from the homepage
In blog/templates/blog/index.html, replace the card title with:
<h3>
<a href="{% url 'post_detail' post.pk %}">
{{ post.title }}
</a>
</h3>
For a post with ID 3, Django turns {% url 'post_detail' post.pk %} into /posts/3/.
Named URLs are safer than typing the path yourself. If the path changes later, the template can keep using the same name.
Step 4 — Create the detail template
Create blog/templates/blog/post_detail.html:
{% load static %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ post.title }} · My First Blog</title>
<link rel="stylesheet" href="{% static 'blog/style.css' %}">
</head>
<body>
<header class="site-header">
<p><a href="{% url 'home' %}">My First Blog</a></p>
<h1>{{ post.title }}</h1>
<p>Published {{ post.created_at|date:"j F Y" }}</p>
</header>
<main class="content">
<article class="post-detail">
{{ post.body|linebreaks }}
</article>
<p><a href="{% url 'home' %}">← Back to all posts</a></p>
</main>
</body>
</html>
linebreaks converts blank lines from the saved body into HTML paragraphs. Do not add |safe to text entered by users.
Step 5 — Add two small styles
Add this to blog/static/blog/style.css:
.post-card a,
.site-header a,
.content a {
color: var(--accent);
text-underline-offset: 3px;
}
.post-detail {
padding: clamp(1.5rem, 5vw, 3rem);
border: 1px solid var(--border);
border-radius: 14px;
background: var(--surface);
}
Step 6 — Walk through the request
Click a post title and say each step aloud:
Click post 3
→ Django receives /posts/3/
→ blog/urls.py sends pk=3 to post_detail
→ get_object_or_404 finds Post 3
→ context sends that post to post_detail.html
→ the template prints the title and body
Then type a missing ID such as /posts/99999/. A 404 page is the correct result.
Optional cleanup — Use a base template
The list and detail templates repeat the HTML document and CSS link. Once everything works, you can learn template inheritance and move the shared shell into base.html.
Do not do this optional refactor if the detail page is still broken. A little duplication is easier to understand than three broken templates.
Checkpoint
- Every homepage title is a link.
- Clicking a title opens
/posts/<number>/. - The detail page shows the full body.
- The back link returns to the homepage.
- A missing post ID returns 404 instead of an error page.
- You can explain where
pkcomes from.
If it does not work
| What you see | Check this |
|---|---|
NoReverseMatch | The URL must have name="post_detail" and receive post.pk. |
post_detail is not defined | Add the function to views.py before refreshing. |
TemplateDoesNotExist | Use blog/templates/blog/post_detail.html. |
| Body appears on one line | Add the linebreaks template filter. |
| Every ID crashes | Import and use get_object_or_404. |
Stop here
Today is complete when the list and detail pages connect through a named URL. Tomorrow you will add one form that validates and saves a new post.
