DEV Community

David Tio
David Tio

Posted on • Originally published at blog.dtio.app

Building a Blog Platform with Docker #2: Tailwind CSS

Building a Blog Platform with Docker #2: Tailwind CSS

Quick one-liner: Upgrade your Flask app to Tailwind CSS via CDN — dark theme with teal branding, no build step required.


Why This Matters

Last time, you got a basic Flask app running with a separate CSS file. It worked — dark background, teal heading, centered layout.

But let's be honest — it's nothing like a blog yet. One centered heading. One paragraph. Nothing a reader would take seriously.

Today, we're making it look a lot more like a blog. We're adding Tailwind CSS.

Why Tailwind? You could write custom CSS. Or use Bootstrap. Or Bulma. But Tailwind is fast, modern, and easy to customise. Plus, it's what I'll use for the final blog.dtio.app, so you're learning the actual stack.

No build step. I'm not making you install Node.js, npm, and a whole build pipeline just for CSS. We're using Tailwind via CDN. It's not production-optimal, but for learning? Perfect.

By the end of this post, you'll have:

  • Tailwind CSS loaded via CDN
  • Google Fonts (DM Sans + Instrument Serif)
  • A teal navigation bar
  • A centered hero section
  • An editorial post list
  • A teal footer

Starting Point

You should have this from last time:

tiohub-blog/
├── app.py
├── static/
│   └── css/
│       └── style.css
└── templates/
    └── index.html
Enter fullscreen mode Exit fullscreen mode

Your index.html currently links to static/css/style.css via a <link> tag in the <head>.

If you don't have this, go back and build Episode 1 first. This one builds on it.


Step 1: Add Tailwind CDN and Google Fonts

Open templates/index.html. You currently have something like this:

<!DOCTYPE html>
<html>
<head>
    <title>Welcome to David Tio's Blog</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
    <h1>Welcome to David Tio's Blog</h1>
    <p>Building a blog platform with Docker.</p>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Add the Google Fonts and Tailwind CDN in the <head>:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>David Tio's Blog</title>
    <link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
    <h1>Welcome to David Tio's Blog</h1>
    <p>Building a blog platform with Docker.</p>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

That's it. Tailwind is now loaded. We'll add the config file in Step 2.

CDN trade-off: It's bigger than a bundled build. But for a personal blog? Nobody's going to notice. And you save hours of build setup.


Step 2: Configure Tailwind

Tailwind's default colors and fonts are a good start, but we want our own brand colors and the fonts we loaded. This can be configured using JavaScript. Let's create a directory for it:

$ mkdir -p static/js
Enter fullscreen mode Exit fullscreen mode

static/js/tailwind.config.js:

tailwind.config = {
    theme: {
        extend: {
            colors: {
                brand: {
                    500: '#14B8A6',  // teal accent
                    600: '#0F766E',  // primary teal
                    700: '#0D5F57',  // darker teal
                }
            },
            fontFamily: {
                sans: ['DM Sans', 'system-ui', 'sans-serif'],
                serif: ['Instrument Serif', 'Georgia', 'serif'],
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Load it in index.html right after the Tailwind CDN script:

<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script src="{{ url_for('static', filename='js/tailwind.config.js') }}"></script>
Enter fullscreen mode Exit fullscreen mode

Now you can use bg-brand-700, text-brand-500, font-serif, etc. in your classes.


Step 3: Build the Navigation Bar

Open templates/index.html. Add this right after the opening <body> tag:

<nav class="bg-brand-700 border-b border-brand-600">
    <div class="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
        <a href="{{ url_for('index') }}" class="font-serif text-xl text-white">
            David Tio's Blog
        </a>
        <div class="flex items-center space-x-6">
            <a href="/" class="text-teal-100 hover:text-white font-medium text-sm transition duration-200">Home</a>
            <a href="#" class="text-teal-100 hover:text-white font-medium text-sm transition duration-200">Series</a>
            <a href="#" class="text-teal-100 hover:text-white font-medium text-sm transition duration-200">About</a>
        </div>
    </div>
</nav>
Enter fullscreen mode Exit fullscreen mode

This gives you:

  • Darker teal navbar (brand-700) with a subtle border line underneath
  • Blog name on the left in Instrument Serif, linked back to your Flask index route via url_for
  • Three navigation links on the right with white hover transitions
  • max-w-6xl mx-auto centers the nav content at a comfortable width

Mobile note: This navbar isn't responsive yet. We'll fix that later. For now, it works on desktop.

Placeholder links: Series and About point to # for now. They're dead links until we build those pages in a future episode. Home links to your Flask index route, which is already live.


Step 4: Build the Hero and Post List

Now replace the entire <body> section with this:

<body class="bg-slate-950 text-gray-100 font-sans min-h-screen flex flex-col">
    <!-- Navbar from Step 3 -->

    <!-- Hero -->
    <div class="max-w-3xl mx-auto px-6 pt-20 pb-12 text-center">
        <span class="inline-block text-brand-500 text-xs font-semibold tracking-widest uppercase mb-5">Docker · Linux · Open Source</span>
        <h1 class="font-serif text-5xl text-white leading-tight mb-5">Practical guides for engineers.</h1>
        <p class="text-gray-400 text-lg leading-relaxed">Building with containers, Linux, and open source tools — one post at a time.</p>
    </div>

    <!-- Posts -->
    <main class="max-w-3xl mx-auto px-6 pb-20 flex-1 w-full">

        <div class="border-t border-slate-800 mb-10"></div>

        <h2 class="text-xs font-semibold text-gray-500 uppercase tracking-widest mb-8">Latest Posts</h2>

        <div class="flex flex-col">

            <article class="border-l-2 border-slate-800 hover:border-brand-500 pl-6 py-5 transition-all duration-300 group cursor-pointer">
                <p class="text-gray-600 text-xs mb-2">29 Mar 2026 &middot; Blog Platform</p>
                <h3 class="text-gray-100 font-semibold text-lg mb-2 group-hover:text-brand-500 transition-colors duration-200">
                    <a href="#">Building a Blog Platform #1: Flask Setup</a>
                </h3>
                <p class="text-gray-500 text-sm leading-relaxed">Get a basic Flask app running with separate CSS — no Docker yet, just Python and a stylesheet.</p>
            </article>

            <div class="border-t border-slate-800 ml-6"></div>

        </div>
    </main>

    <!-- Footer from Step 5 -->
</body>
Enter fullscreen mode Exit fullscreen mode

What's happening here:

  • bg-slate-950 gives the whole page a dark slate background
  • min-h-screen flex flex-col makes the body stretch to full viewport height and stacks nav, hero, posts, and footer vertically
  • max-w-3xl mx-auto centers the content at a readable width — just like a real blog
  • The hero has a small teal label ("Docker · Linux · Open Source") and a large serif heading
  • The post card has a left border that turns teal when you hover over it — try it
  • The url_for in the navbar links back to your Flask index route, so nothing is hardcoded

Step 5: Add a Footer

Before the closing </body> tag, add:

<footer class="bg-brand-700 border-t border-brand-600">
    <div class="max-w-6xl mx-auto px-6 py-6 flex items-center justify-between">
        <p class="text-teal-100 text-sm">&copy; 2026 David Tio.</p>
        <div class="flex space-x-6">
            <a href="#" class="text-teal-100 hover:text-white text-sm transition-colors duration-200">LinkedIn</a>
            <a href="#" class="text-teal-100 hover:text-white text-sm transition-colors duration-200">Twitter</a>
            <a href="#" class="text-teal-100 hover:text-white text-sm transition-colors duration-200">GitHub</a>
        </div>
    </div>
</footer>
Enter fullscreen mode Exit fullscreen mode

Matches the nav — teal background, copyright left, social links right.


Step 6: Clean Up Your CSS

Now that Tailwind is doing all the work, your old style.css is redundant. Remove the <link> tag from the <head> of index.html:

<!-- Delete this line -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
Enter fullscreen mode Exit fullscreen mode

Then delete the file itself:

$ rm static/css/style.css
Enter fullscreen mode Exit fullscreen mode

Step 7: Test It

If you closed the terminal since Episode 1, reactivate the venv first:

$ source venv/bin/activate
Enter fullscreen mode Exit fullscreen mode

Run your Flask app:

$ python app.py
Enter fullscreen mode Exit fullscreen mode

Visit http://localhost:8000. You should see:

  • Teal navbar with your name in Instrument Serif
  • Dark background with a centered hero
  • Editorial post list with teal hover effects
  • Matching teal footer

It should look like an actual blog now.


What You've Built

You now have:

  • Tailwind CSS via CDN (no build step)
  • Google Fonts: DM Sans body, Instrument Serif for headings
  • Custom brand colors (teal-500 accent, teal-700 nav/footer)
  • Dark slate background (slate-950)
  • Teal nav and footer branding
  • Centered hero with serif heading
  • Editorial post list with teal left-border hover

tiohub-blog running with Tailwind CSS dark theme, teal nav and footer, editorial post list


Coming Up

Right now, your posts are hardcoded HTML. You want to write Markdown files and have them render automatically. Next time: Markdown support. You'll write .md files with frontmatter, and Flask will parse them into HTML.

No more HTML templates for every blog post. Just write Markdown and go.


Found this helpful? Share it with your network or drop a comment below.

Top comments (0)