Skip to the resource

Day 3 of 7 · 60–90 minutes

Make the blog look good with CSS

Create Django’s static-file folders, connect one stylesheet, and add a small responsive design.

Today you finish withA clean, responsive blog page with styled post cards.

Today’s goal

Connect one CSS file through Django’s static-file system. Then give the page a clean layout that also works on a phone.

The HTML template owns structure. The CSS file owns appearance.

Step 1 — Create the static folders

Inside blog, create:

blog/
└── static/
    └── blog/
        └── style.css

The full app structure now includes:

blog/
├── static/blog/style.css
├── templates/blog/index.html
├── urls.py
└── views.py

Step 2 — Load the stylesheet

At the very top of blog/templates/blog/index.html, before <!doctype html>, add:

{% load static %}

Inside <head>, below the <title>, add:

<link rel="stylesheet" href="{% static 'blog/style.css' %}">

Step 3 — Add useful classes to the HTML

Change the opening header and main tags:

<header class="site-header">
<main class="content">

Wrap the loop in a grid and add a class to each article:

{% 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>
{% else %}
    <p class="empty-state">No posts have been published yet.</p>
{% endif %}

Step 4 — Add the CSS

Put this in blog/static/blog/style.css:

:root {
    --page: #f4f7fb;
    --surface: #ffffff;
    --text: #172033;
    --muted: #617086;
    --accent: #1769d2;
    --border: #d9e2ee;
}

* {
    box-sizing: border-box;
}

body {
    margin: 0;
    background: var(--page);
    color: var(--text);
    font-family: system-ui, sans-serif;
    line-height: 1.6;
}

.site-header,
.content {
    width: min(1000px, calc(100% - 2rem));
    margin-inline: auto;
}

.site-header {
    padding: 4rem 0 2rem;
}

.site-header h1 {
    margin: 0;
    font-size: clamp(2.5rem, 8vw, 5rem);
    line-height: 1;
}

.site-header p {
    max-width: 600px;
    color: var(--muted);
}

.content {
    padding-bottom: 4rem;
}

.post-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: 1rem;
}

.post-card,
.empty-state {
    padding: 1.5rem;
    border: 1px solid var(--border);
    border-radius: 14px;
    background: var(--surface);
}

.post-card h3 {
    margin-top: 0;
    color: var(--accent);
}

.post-card p {
    margin-bottom: 0;
    color: var(--muted);
}

.empty-state {
    text-align: center;
}

a:focus-visible,
button:focus-visible,
input:focus-visible {
    outline: 3px solid var(--accent);
    outline-offset: 3px;
}

Step 5 — Prove the static file is connected

Refresh the page. You should see a pale background and white post cards.

If nothing changes, temporarily add this to the bottom of the CSS:

body {
    border-top: 10px solid red;
}

If the red line appears, CSS is connected. Remove the temporary rule.

If it does not appear:

  1. Confirm {% load static %} is the first line.
  2. Confirm the path is blog/static/blog/style.css.
  3. Confirm the link says {% static 'blog/style.css' %}.
  4. Hard-refresh the browser.

Step 6 — Check the phone layout

Make the browser narrow. The grid should automatically become one column because of:

grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));

No media query is needed for this small layout.

Checkpoint

  • The page loads /static/blog/style.css.
  • Post cards appear in columns on a wide screen.
  • Cards become one column when the screen is narrow.
  • The HTML has no large <style> block.
  • Pressing Tab shows a visible focus outline.

Stop here

Today is complete when HTML and CSS have separate jobs. Tomorrow you will add a search box and learn how text travels from the browser into a Django view.