Skip to the resource

Day 4 of 7 · 60–90 minutes

Add a simple search box

Send a search with GET, read it from request.GET, and filter the temporary Python list.

Today you finish withVisitors can search the sample posts by title or description.

Today’s goal

Add a search box that filters the three temporary posts. Search uses GET, so the search text appears in the URL and can be refreshed or shared.

After searching for python, the address will look like:

http://127.0.0.1:8000/?q=python

Step 1 — Add the search form

In blog/templates/blog/index.html, place this inside <main class="content">, before the Latest posts heading:

<form class="search-form" method="get" action="{% url 'home' %}">
    <label for="search">Search posts</label>

    <div class="search-row">
        <input
            id="search"
            type="search"
            name="q"
            value="{{ query }}"
            placeholder="Try Python or Django"
        >
        <button type="submit">Search</button>
    </div>
</form>

The input’s name="q" creates the ?q=... part of the URL. The value must be passed back through context so the input remembers the search after the page reloads.

Submit the form now. The URL should change, but all posts still appear. That is correct—the view has not filtered anything yet.

Step 2 — Read the search in the view

At the beginning of home() in blog/views.py, add:

query = request.GET.get("q", "").strip()

Then add query to context:

context = {
    "site_title": "My First Blog",
    "posts": posts,
    "query": query,
}

.get("q", "") safely returns an empty string when the URL has no q. .strip() removes spaces from the beginning and end.

Step 3 — Filter the sample posts

After the posts list and before context, add:

if query:
    query_lower = query.lower()
    posts = [
        post
        for post in posts
        if query_lower in post["title"].lower()
        or query_lower in post["description"].lower()
    ]

Read it as a sentence:

Keep a post when the lower-case search text appears in its lower-case title or description.

Step 4 — Show a useful result heading

Replace the fixed Latest posts heading with:

{% if query %}
    <h2>Search results for “{{ query }}”</h2>
{% else %}
    <h2>Latest posts</h2>
{% endif %}

Replace the empty message with one that understands search:

{% if posts %}
    <div class="post-grid">
        {% for post in posts %}
            <article class="post-card">
                <h3>{{ post.title }}</h3>
                <p>{{ post.description }}</p>
            </article>
        {% endfor %}
    </div>
{% elif query %}
    <div class="empty-state">
        <p>No posts matched “{{ query }}”.</p>
        <a href="{% url 'home' %}">Clear the search</a>
    </div>
{% else %}
    <p class="empty-state">No posts have been published yet.</p>
{% endif %}

Step 5 — Style the form

Add this to blog/static/blog/style.css:

.search-form {
    margin-bottom: 2rem;
    padding: 1rem;
    border: 1px solid var(--border);
    border-radius: 14px;
    background: var(--surface);
}

.search-form label {
    display: block;
    margin-bottom: 0.5rem;
    font-weight: 700;
}

.search-row {
    display: flex;
    gap: 0.75rem;
}

.search-row input {
    min-width: 0;
    flex: 1;
    padding: 0.75rem;
    border: 1px solid var(--border);
    border-radius: 8px;
    font: inherit;
}

.search-row button {
    padding: 0.75rem 1rem;
    border: 0;
    border-radius: 8px;
    background: var(--accent);
    color: white;
    cursor: pointer;
    font: inherit;
    font-weight: 700;
}

@media (max-width: 520px) {
    .search-row {
        flex-direction: column;
    }
}

Step 6 — Test seven small cases

  1. Visit / — all posts appear.
  2. Search Python — the Python post appears.
  3. Search django — the Django post appears.
  4. Search page — matching text in a description also works.
  5. Search PYTHON — capitalization does not matter.
  6. Search spaceship — the no-match message appears.
  7. Clear the search — all posts return.

Checkpoint

  • The URL contains ?q= after a search.
  • The search input keeps its value after reload.
  • Matching ignores upper- and lower-case differences.
  • A bad search shows a useful message and clear link.
  • You can explain that the form sends data and the view filters data.

If it does not work

What you seeCheck this
URL changes but posts do notFiltering must run before context is built.
Input becomes emptyAdd query to context and value="{{ query }}".
KeyError: qUse request.GET.get("q", ""), not request.GET["q"].
Search is case-sensitiveUse .lower() on both the query and post text.

Stop here

Today is complete when the browser sends a search and the view returns only matching posts. Tomorrow you will replace the temporary dictionaries with real records saved in SQLite.