Skip to the resource

Day 6 of 7 · 60–90 minutes

Open each post on its own page

Add a URL containing a post ID, find that post safely, and render a detail template.

Today you finish withEvery post title opens a page with the full article.

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.

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 pk comes from.

If it does not work

What you seeCheck this
NoReverseMatchThe URL must have name="post_detail" and receive post.pk.
post_detail is not definedAdd the function to views.py before refreshing.
TemplateDoesNotExistUse blog/templates/blog/post_detail.html.
Body appears on one lineAdd the linebreaks template filter.
Every ID crashesImport 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.