DEV Community

David Tio
David Tio

Posted on • Originally published at blog.dtio.app

Building a Blog Platform with Docker #4: Dynamic Routes and a Real Post List

๐Ÿ—‚๏ธ Building a Blog Platform with Docker #4: Dynamic Routes and a Real Post List

Quick one-liner: Replace the hardcoded route and static post list with a dynamic scanner that finds every .md file in your content folder โ€” drop a file in, it appears on the homepage.


๐Ÿค” Why This Matters

After Episode 3 you can render a Markdown post. One post. At a hardcoded URL.

That's proof the plumbing works โ€” but it's not a blog platform. To have a real blog, two things need to change:

  1. The homepage should list all posts, sorted by date, pulled from actual files โ€” not copied in by hand
  2. The routes should be dynamic โ€” any post at any date and slug should just work, without adding a new @app.route every time

While we're at it, there's a third thing worth doing now: SEO. The moment you have multiple real posts and real URLs, Google can find them. A <meta name="description"> tag is the minimum you need. And since we already have a description field in the post frontmatter (we're adding it today), it costs nothing to wire it up.

By the end of this episode, you'll be able to drop any .md file into content/posts/, refresh the homepage, and see it listed. No code changes required.


๐Ÿ Starting Point

From Episode 3, your structure looks like this:

tiohub-blog/
โ”œโ”€โ”€ app.py
โ”œโ”€โ”€ requirements.txt
โ”œโ”€โ”€ static/
โ”‚   โ””โ”€โ”€ js/
โ”‚       โ”œโ”€โ”€ code-blocks.js
โ”‚       โ””โ”€โ”€ tailwind.config.js
โ”œโ”€โ”€ templates/
โ”‚   โ”œโ”€โ”€ index.html
โ”‚   โ””โ”€โ”€ post.html
โ””โ”€โ”€ content/
    โ””โ”€โ”€ posts/
        โ””โ”€โ”€ hey-markdown.md
Enter fullscreen mode Exit fullscreen mode

If you're missing anything, go back to Episode 3 first.


โœ๏ธ Step 1: Update the Frontmatter Schema

We're adding one field to the frontmatter: description. It does two things at once โ€” it becomes the <meta name="description"> tag for SEO, and it becomes the excerpt blurb on the homepage post list.

Update content/posts/hey-markdown.md. Find the existing frontmatter:

---
title: "Hey Markdown"
date: 2026-04-25
tags: [meta, blog]
---
Enter fullscreen mode Exit fullscreen mode

Add the description field between date and tags:

---
title: "Hey Markdown"
date: 2026-04-25
description: "The first post written in Markdown โ€” no more writing HTML by hand."
tags: [meta, blog]
---
Enter fullscreen mode Exit fullscreen mode

This is the full frontmatter schema going forward. Every post should have all four fields:

Field Used for
title <title> tag, <h1> on post page, post list
date URL generation, sorting, display
description <meta name="description">, excerpt on homepage
tags Tag pills on post page, eventually tag pages in Ep12

๐Ÿ” Step 2: Add the Post Scanner

Open app.py. Add a get_all_posts() function below parse_post():

def get_all_posts():
    posts = []
    posts_dir = 'content/posts'
    for filename in os.listdir(posts_dir):
        if not filename.endswith('.md'):
            continue
        filepath = os.path.join(posts_dir, filename)
        post = parse_post(filepath)
        post['slug'] = filename[:-3]
        posts.append(post)
    posts.sort(key=lambda p: p.get('date', ''), reverse=True)
    return posts
Enter fullscreen mode Exit fullscreen mode

What this does:

  • Lists every .md file in content/posts/
  • Parses each one with the existing parse_post() function
  • Adds a slug field (the filename without .md) โ€” this is what goes in the URL
  • Sorts all posts by date, newest first
  • Returns the full list

๐Ÿ”— Step 3: Replace the Hardcoded Route with a Dynamic One

Remove the hardcoded hey_markdown_post() route entirely. Replace it with this:

@app.route('/<int:year>/<int:month>/<slug>')
def post(year, month, slug):
    filepath = f'content/posts/{slug}.md'
    if not os.path.exists(filepath):
        abort(404)
    post = parse_post(filepath)
    return render_template('post.html', post=post)
Enter fullscreen mode Exit fullscreen mode

At the top of app.py, find the existing Flask import:

from flask import Flask, render_template
Enter fullscreen mode Exit fullscreen mode

Add abort to it:

from flask import Flask, render_template, abort
Enter fullscreen mode Exit fullscreen mode

How the URL is constructed: The date in the frontmatter has a year and month attribute once parsed by PyYAML (it parses 2026-04-25 as a Python date object). We'll use those in the next step to build the correct link from the homepage. The route parameters year and month aren't used to look up the file โ€” the slug is enough, since slugs are unique. They're there to keep the URL format consistent with the Blogger pattern we established in Episode 3.


๐Ÿ  Step 4: Update the Homepage

Update the index route in app.py. Find the existing route:

@app.route('/')
def index():
    return render_template('index.html')
Enter fullscreen mode Exit fullscreen mode

Replace it with:

@app.route('/')
def index():
    posts = get_all_posts()
    return render_template('index.html', posts=posts)
Enter fullscreen mode Exit fullscreen mode

Replace the hardcoded post list in templates/index.html:

Find the hardcoded <article> block inside <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>
Enter fullscreen mode Exit fullscreen mode

Replace it with this loop:

{% for post in posts %}
<article class="border-b border-slate-800 pb-10 last:border-0 last:pb-0">
    <div class="flex items-center gap-3 mb-3">
        <time class="text-gray-500 text-sm">{{ post.date }}</time>
        {% for tag in post.tags %}
        <span class="text-xs bg-teal-900/50 text-teal-300 px-2 py-0.5 rounded-full">{{ tag }}</span>
        {% endfor %}
    </div>
    <h2 class="font-serif text-2xl text-white mb-3 leading-snug">
        <a href="/{{ post.date.year }}/{{ '%02d' | format(post.date.month) }}/{{ post.slug }}"
           class="hover:text-teal-400 transition-colors duration-200">
            {{ post.title }}
        </a>
    </h2>
    <p class="text-gray-400 leading-relaxed mb-4">{{ post.description }}</p>
    <a href="/{{ post.date.year }}/{{ '%02d' | format(post.date.month) }}/{{ post.slug }}"
       class="text-teal-400 text-sm hover:text-teal-300 font-medium transition-colors duration-200">
        Read more โ†’
    </a>
</article>
{% endfor %}
Enter fullscreen mode Exit fullscreen mode

Each post in the list now shows:

  • The date and tag pills in a row
  • The post title as a link
  • The description as the excerpt
  • A "Read more โ†’" link

The URL is built from the date and slug: /2026/04/hey-markdown. No hardcoding anywhere.


๐Ÿ”Ž Step 5: Add Meta Description to the Post Page

Open templates/post.html. In the <head> section, add the meta description tag after <title>:

<title>{{ post.title }}</title>
<meta name="description" content="{{ post.description }}">
Enter fullscreen mode Exit fullscreen mode

That's it. When Google indexes the page, it reads this tag and uses it as the snippet under the link in search results. The content comes directly from the description field in your frontmatter โ€” the same text you already wrote for the homepage excerpt.


๐Ÿท๏ธ Step 6: Add Tag Pills to the Post Page

Open templates/post.html. Inside <main>, find the date and title block:

<p class="text-gray-500 text-sm mb-4">{{ post.date }}</p>
<h1 class="font-serif text-4xl text-white leading-tight mb-8">{{ post.title }}</h1>
Enter fullscreen mode Exit fullscreen mode

Replace it with this โ€” same date and title, with the tag pills inserted between them:

<p class="text-gray-500 text-sm mb-3">{{ post.date }}</p>
<div class="flex flex-wrap gap-2 mb-6">
    {% for tag in post.tags %}
    <span class="text-xs bg-teal-900/50 text-teal-300 px-2.5 py-1 rounded-full">{{ tag }}</span>
    {% endfor %}
</div>
<h1 class="font-serif text-4xl text-white leading-tight mb-8">{{ post.title }}</h1>
Enter fullscreen mode Exit fullscreen mode

Tags are non-clickable for now โ€” they're labels, not links. They become proper links with their own pages in Episode 12, once the platform has enough posts and structure to make tag filtering useful.


๐Ÿงช Step 7: Test It

Add a second post so you can verify the list actually works. Create content/posts/second-post.md:

---
title: "Second Post"
date: 2026-04-26
description: "Proving the scanner works โ€” this post appeared without touching app.py."
tags: [test]
---

Dropped this file into `content/posts/`. Restarted Flask. It appeared.

No new routes. No hardcoded HTML. Just a file.
Enter fullscreen mode Exit fullscreen mode

Run the app:

$ python app.py
Enter fullscreen mode Exit fullscreen mode

Check:

  • Homepage โ€” both posts listed, newest first, with excerpts and tags
  • /2026/04/hey-markdown โ€” first post renders correctly
  • /2026/04/second-post โ€” second post renders correctly
  • View source on either post โ€” confirm <meta name="description"> is present in <head>
  • Drop another .md file into content/posts/ and refresh โ€” it should appear immediately

โœ… What You've Built

Your file structure now:

tiohub-blog/
โ”œโ”€โ”€ app.py
โ”œโ”€โ”€ requirements.txt
โ”œโ”€โ”€ static/
โ”‚   โ””โ”€โ”€ js/
โ”‚       โ”œโ”€โ”€ tailwind.config.js
โ”‚       โ””โ”€โ”€ code-blocks.js
โ”œโ”€โ”€ templates/
โ”‚   โ”œโ”€โ”€ index.html
โ”‚   โ””โ”€โ”€ post.html
โ””โ”€โ”€ content/
    โ””โ”€โ”€ posts/
        โ”œโ”€โ”€ hey-markdown.md
        โ””โ”€โ”€ second-post.md
Enter fullscreen mode Exit fullscreen mode

What changed:

โœ… get_all_posts() โ€” scans the posts folder and returns all posts sorted by date
โœ… Dynamic /<int:year>/<int:month>/<slug> route โ€” works for any post, no code changes needed
โœ… Homepage driven by real files โ€” no more hardcoded HTML
โœ… description field wired up as SEO meta tag and homepage excerpt
โœ… Tag pills on post pages


๐Ÿš€ Coming Up

Next episode: Docker. The app runs fine locally, but "works on my machine" isn't a deployment strategy. We'll write a Dockerfile, build an image, and run the blog in a container โ€” the same way it'll run in production.


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


SEO Metadata

  • Title: Building a Blog Platform with Docker #4: Dynamic Routes and a Real Post List
  • Meta Description: Replace hardcoded Flask routes with a dynamic post scanner. Every .md file in your content folder becomes a post automatically โ€” plus SEO meta description and tag pills.
  • Word Count: ~1,100

Top comments (0)